diff --git a/README.md b/README.md index 342a4c13f..b2bd01d99 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.50 - 1.21.90 and Minecraft Java 1.21.5. For more information, please see [here](https://geysermc.org/wiki/geyser/supported-versions/). +Geyser is currently supporting Minecraft Bedrock 1.21.70 - 1.21.90 and Minecraft Java 1.21.6. 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/connection/GeyserConnection.java b/api/src/main/java/org/geysermc/geyser/api/connection/GeyserConnection.java index ede4c1bc4..b7a678aa6 100644 --- a/api/src/main/java/org/geysermc/geyser/api/connection/GeyserConnection.java +++ b/api/src/main/java/org/geysermc/geyser/api/connection/GeyserConnection.java @@ -65,6 +65,12 @@ public interface GeyserConnection extends Connection, CommandSource { */ int ping(); + /** + * @return {@code true} if the client currently has a form open. + * @since 2.8.0 + */ + boolean hasFormOpen(); + /** * Closes the currently open form on the client. */ @@ -75,6 +81,43 @@ public interface GeyserConnection extends Connection, CommandSource { */ int protocolVersion(); + /** + * Attempts to open the {@code minecraft:pause_screen_additions} dialog tag. This method opens this dialog the same way Java does, that is: + * + * + * + *

Use {@link GeyserConnection#hasFormOpen()} to check if a dialog was opened.

+ * @since 2.8.0 + */ + void openPauseScreenAdditions(); + + /** + * Attempts to open the {@code minecraft:quick_actions} dialog tag. This method opens this dialog the same way Java does, that is: + * + * + * + *

Use {@link GeyserConnection#hasFormOpen()} to check if a dialog was opened.

+ * @since 2.8.0 + */ + void openQuickActions(); + + /** + * Sends a command as if the player had executed it. + * + * @param command the command without the leading forward-slash + * @since 2.8.0 + */ + void sendCommand(String command); + /** * @param javaId the Java entity ID to look up. * @return a {@link GeyserEntity} if present in this connection's entity tracker. diff --git a/bootstrap/mod/fabric/src/main/resources/fabric.mod.json b/bootstrap/mod/fabric/src/main/resources/fabric.mod.json index c0056e5cf..69071cea4 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.5" + "minecraft": ">=1.21.6" } } diff --git a/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/PermissionUtils.java b/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/PermissionUtils.java index c57dc9a6c..5755e407f 100644 --- a/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/PermissionUtils.java +++ b/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/PermissionUtils.java @@ -32,6 +32,8 @@ import org.geysermc.geyser.api.event.lifecycle.GeyserRegisterPermissionsEvent; import org.geysermc.geyser.api.util.TriState; import org.geysermc.geyser.platform.neoforge.mixin.PermissionNodeMixin; +import java.util.Objects; + /** * Common logic for handling the more complicated way we have to register permission on NeoForge */ @@ -69,7 +71,7 @@ public class PermissionUtils { case FALSE -> false; case NOT_SET -> { if (player != null) { - yield player.createCommandSourceStack().hasPermission(player.server.getOperatorUserPermissionLevel()); + yield player.createCommandSourceStack().hasPermission(Objects.requireNonNull(player.getServer()).getOperatorUserPermissionLevel()); } yield false; // NeoForge javadocs say player is null in the case of an offline player. } diff --git a/bootstrap/mod/neoforge/src/main/resources/META-INF/neoforge.mods.toml b/bootstrap/mod/neoforge/src/main/resources/META-INF/neoforge.mods.toml index 7958926bc..3f1fe574d 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.5.0-beta,)" + versionRange="[21.6.0-beta,)" ordering="NONE" side="BOTH" [[dependencies.geyser_neoforge]] modId="minecraft" type="required" - versionRange="[1.21.5,)" + versionRange="[1.21.6,)" ordering="NONE" side="BOTH" diff --git a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/ModPingPassthrough.java b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/ModPingPassthrough.java index e1f9c01e2..2b2e3b736 100644 --- a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/ModPingPassthrough.java +++ b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/ModPingPassthrough.java @@ -25,22 +25,20 @@ package org.geysermc.geyser.platform.mod; +import com.mojang.serialization.JsonOps; +import io.netty.channel.ChannelFutureListener; import lombok.AllArgsConstructor; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; -import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; -import net.minecraft.core.RegistryAccess; import net.minecraft.network.Connection; -import net.minecraft.network.PacketSendListener; +import net.minecraft.network.chat.ComponentSerialization; import net.minecraft.network.protocol.Packet; import net.minecraft.network.protocol.PacketFlow; import net.minecraft.network.protocol.status.ClientboundStatusResponsePacket; import net.minecraft.network.protocol.status.ServerStatus; import net.minecraft.network.protocol.status.ServerStatusPacketListener; import net.minecraft.network.protocol.status.ServerboundStatusRequestPacket; +import net.minecraft.resources.RegistryOps; import net.minecraft.server.MinecraftServer; import net.minecraft.server.network.ServerStatusPacketListenerImpl; -import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.geyser.GeyserLogger; import org.geysermc.geyser.ping.GeyserPingInfo; @@ -52,9 +50,6 @@ import java.util.Objects; @AllArgsConstructor public class ModPingPassthrough implements IGeyserPingPassthrough { - private static final GsonComponentSerializer GSON_SERIALIZER = GsonComponentSerializer.gson(); - private static final LegacyComponentSerializer LEGACY_SERIALIZER = LegacyComponentSerializer.legacySection(); - private final MinecraftServer server; private final GeyserLogger logger; @@ -81,7 +76,7 @@ public class ModPingPassthrough implements IGeyserPingPassthrough { } return new GeyserPingInfo( - net.minecraft.network.chat.Component.Serializer.toJson(status.description(), RegistryAccess.EMPTY), + ComponentSerialization.CODEC.encodeStart(RegistryOps.create(JsonOps.INSTANCE, server.registryAccess()), status.description()).getOrThrow().toString(), status.players().map(ServerStatus.Players::max).orElse(1), status.players().map(ServerStatus.Players::online).orElse(0) ); @@ -99,11 +94,11 @@ public class ModPingPassthrough implements IGeyserPingPassthrough { } @Override - public void send(@NonNull Packet packet, @Nullable PacketSendListener packetSendListener, boolean bl) { + public void send(Packet packet, @Nullable ChannelFutureListener channelFutureListener, boolean bl) { if (packet instanceof ClientboundStatusResponsePacket statusResponse) { status = statusResponse.status(); } - super.send(packet, packetSendListener, bl); + super.send(packet, channelFutureListener, bl); } } } diff --git a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/command/ModCommandSource.java b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/command/ModCommandSource.java index af1f368b3..f34164b5a 100644 --- a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/command/ModCommandSource.java +++ b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/command/ModCommandSource.java @@ -25,10 +25,13 @@ package org.geysermc.geyser.platform.mod.command; +import com.google.gson.JsonElement; +import com.mojang.serialization.JsonOps; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; import net.minecraft.commands.CommandSourceStack; -import net.minecraft.core.RegistryAccess; import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.ComponentSerialization; +import net.minecraft.resources.RegistryOps; import net.minecraft.server.level.ServerPlayer; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; @@ -36,7 +39,6 @@ import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.command.GeyserCommandSource; import org.geysermc.geyser.text.ChatColor; -import java.util.Objects; import java.util.UUID; public class ModCommandSource implements GeyserCommandSource { @@ -65,8 +67,8 @@ public class ModCommandSource implements GeyserCommandSource { @Override public void sendMessage(net.kyori.adventure.text.Component message) { if (source.getEntity() instanceof ServerPlayer player) { - String decoded = GsonComponentSerializer.gson().serialize(message); - player.displayClientMessage(Objects.requireNonNull(Component.Serializer.fromJson(decoded, RegistryAccess.EMPTY)), false); + JsonElement jsonComponent = GsonComponentSerializer.gson().serializeToTree(message); + player.displayClientMessage(ComponentSerialization.CODEC.parse(RegistryOps.create(JsonOps.INSTANCE, player.registryAccess()), jsonComponent).getOrThrow(), false); return; } GeyserCommandSource.super.sendMessage(message); diff --git a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/world/GeyserModWorldManager.java b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/world/GeyserModWorldManager.java index f791aab7a..5ede434c9 100644 --- a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/world/GeyserModWorldManager.java +++ b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/world/GeyserModWorldManager.java @@ -25,7 +25,6 @@ package org.geysermc.geyser.platform.mod.world; -import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; import net.minecraft.SharedConstants; import net.minecraft.core.BlockPos; import net.minecraft.core.registries.BuiltInRegistries; @@ -50,7 +49,6 @@ import java.util.function.Consumer; public class GeyserModWorldManager extends GeyserWorldManager { - private static final GsonComponentSerializer GSON_SERIALIZER = GsonComponentSerializer.gson(); private final MinecraftServer server; public GeyserModWorldManager(MinecraftServer server) { @@ -62,7 +60,7 @@ public class GeyserModWorldManager extends GeyserWorldManager { // If the protocol version of Geyser and the server are not the // same, fallback to the chunk cache. May be able to update this // in the future to use ViaVersion however, like Spigot does. - if (SharedConstants.getCurrentVersion().getProtocolVersion() != GameProtocol.getJavaProtocolVersion()) { + if (SharedConstants.getCurrentVersion().protocolVersion() != GameProtocol.getJavaProtocolVersion()) { return super.getBlockAt(session, x, y, z); } @@ -96,7 +94,7 @@ public class GeyserModWorldManager extends GeyserWorldManager { @Override public boolean hasOwnChunkCache() { - return SharedConstants.getCurrentVersion().getProtocolVersion() == GameProtocol.getJavaProtocolVersion(); + return SharedConstants.getCurrentVersion().protocolVersion() == GameProtocol.getJavaProtocolVersion(); } @Override diff --git a/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneBootstrap.java b/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneBootstrap.java index 03ae18408..60bd959d2 100644 --- a/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneBootstrap.java +++ b/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneBootstrap.java @@ -236,6 +236,9 @@ public class GeyserStandaloneBootstrap implements GeyserBootstrap { // Event must be fired after CommandRegistry has subscribed its listener. // Also, the subscription for the Permissions class is created when Geyser is initialized. cloud.fireRegisterPermissionsEvent(); + } else { + // This isn't ideal - but geyserLogger#start won't ever finish, leading to a reloading deadlock + geyser.setReloading(false); } if (gui != null) { diff --git a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityPingPassthrough.java b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityPingPassthrough.java index 0db3c662a..a61a65805 100644 --- a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityPingPassthrough.java +++ b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityPingPassthrough.java @@ -26,6 +26,7 @@ package org.geysermc.geyser.platform.velocity; import com.velocitypowered.api.event.proxy.ProxyPingEvent; +import com.velocitypowered.api.network.HandshakeIntent; import com.velocitypowered.api.network.ProtocolState; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.proxy.InboundConnection; @@ -84,6 +85,11 @@ public class GeyserVelocityPingPassthrough implements IGeyserPingPassthrough { return Optional.empty(); } + @Override + public Optional getRawVirtualHost() { + return Optional.empty(); + } + @Override public boolean isActive() { return false; @@ -98,6 +104,11 @@ public class GeyserVelocityPingPassthrough implements IGeyserPingPassthrough { public ProtocolState getProtocolState() { return ProtocolState.STATUS; } + + @Override + public HandshakeIntent getHandshakeIntent() { + return HandshakeIntent.STATUS; + } } } diff --git a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java index 28b306c66..65e0ed4e5 100644 --- a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java +++ b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java @@ -185,7 +185,8 @@ public class GeyserImpl implements GeyserApi, EventRegistrar { /** * Determines if we're currently reloading. Replaces per-bootstrap reload checks */ - private volatile boolean isReloading; + @Setter + private boolean isReloading; /** * Determines if Geyser is currently enabled. This is used to determine if {@link #disable()} should be called during {@link #shutdown()}. diff --git a/core/src/main/java/org/geysermc/geyser/command/CommandRegistry.java b/core/src/main/java/org/geysermc/geyser/command/CommandRegistry.java index 9a160e378..75ea81ff9 100644 --- a/core/src/main/java/org/geysermc/geyser/command/CommandRegistry.java +++ b/core/src/main/java/org/geysermc/geyser/command/CommandRegistry.java @@ -45,12 +45,14 @@ import org.geysermc.geyser.api.util.TriState; import org.geysermc.geyser.command.defaults.AdvancedTooltipsCommand; import org.geysermc.geyser.command.defaults.AdvancementsCommand; import org.geysermc.geyser.command.defaults.ConnectionTestCommand; +import org.geysermc.geyser.command.defaults.CustomOptionsCommand; import org.geysermc.geyser.command.defaults.DumpCommand; import org.geysermc.geyser.command.defaults.ExtensionsCommand; import org.geysermc.geyser.command.defaults.HelpCommand; import org.geysermc.geyser.command.defaults.ListCommand; import org.geysermc.geyser.command.defaults.OffhandCommand; import org.geysermc.geyser.command.defaults.PingCommand; +import org.geysermc.geyser.command.defaults.QuickActionsCommand; import org.geysermc.geyser.command.defaults.ReloadCommand; import org.geysermc.geyser.command.defaults.SettingsCommand; import org.geysermc.geyser.command.defaults.StatisticsCommand; @@ -166,6 +168,9 @@ public class CommandRegistry implements EventRegistrar { registerBuiltInCommand(new AdvancedTooltipsCommand("tooltips", "geyser.commands.advancedtooltips.desc", "geyser.command.tooltips")); registerBuiltInCommand(new ConnectionTestCommand(geyser, "connectiontest", "geyser.commands.connectiontest.desc", "geyser.command.connectiontest")); registerBuiltInCommand(new PingCommand("ping", "geyser.commands.ping.desc", "geyser.command.ping")); + registerBuiltInCommand(new CustomOptionsCommand("options", "geyser.commands.options.desc", "geyser.command.options")); + registerBuiltInCommand(new QuickActionsCommand("quickactions", "geyser.commands.quickactions.desc", "geyser.command.quickactions")); + if (this.geyser.getPlatformType() == PlatformType.STANDALONE) { registerBuiltInCommand(new StopCommand(geyser, "stop", "geyser.commands.stop.desc", "geyser.command.stop")); } @@ -282,7 +287,7 @@ public class CommandRegistry implements EventRegistrar { help.execute(source); } else if (STANDALONE_COMMAND_MANAGER && source instanceof GeyserSession session) { // If we are on an appropriate platform, forward the command to the backend - session.sendCommand(context.rawInput().input()); + session.sendCommandPacket(context.rawInput().input()); } else { source.sendLocaleString(ExceptionHandlers.PERMISSION_FAIL_LANG_KEY); } diff --git a/core/src/main/java/org/geysermc/geyser/command/ExceptionHandlers.java b/core/src/main/java/org/geysermc/geyser/command/ExceptionHandlers.java index 6348b8515..444057709 100644 --- a/core/src/main/java/org/geysermc/geyser/command/ExceptionHandlers.java +++ b/core/src/main/java/org/geysermc/geyser/command/ExceptionHandlers.java @@ -89,7 +89,7 @@ final class ExceptionHandlers { (ctx, e) -> { // Let backend server receive & handle the command if (CommandRegistry.STANDALONE_COMMAND_MANAGER && ctx.sender() instanceof GeyserSession session) { - session.sendCommand(ctx.rawInput().input()); + session.sendCommandPacket(ctx.rawInput().input()); } else { ctx.sender().sendLocaleString("geyser.command.not_found"); } @@ -114,7 +114,7 @@ final class ExceptionHandlers { // Let backend server receive & handle the command if (CommandRegistry.STANDALONE_COMMAND_MANAGER && source instanceof GeyserSession session) { - session.sendCommand(context.rawInput().input()); + session.sendCommandPacket(context.rawInput().input()); return; } diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/CustomOptionsCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/CustomOptionsCommand.java new file mode 100644 index 000000000..32d151da5 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/command/defaults/CustomOptionsCommand.java @@ -0,0 +1,51 @@ +/* + * 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.command.defaults; + +import org.geysermc.geyser.api.util.TriState; +import org.geysermc.geyser.command.GeyserCommand; +import org.geysermc.geyser.command.GeyserCommandSource; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.text.GeyserLocale; +import org.incendo.cloud.context.CommandContext; + +import java.util.Objects; + +public class CustomOptionsCommand extends GeyserCommand { + + public CustomOptionsCommand(String name, String description, String permission) { + super(name, description, permission, TriState.TRUE, true, true); + } + + @Override + public void execute(CommandContext context) { + GeyserSession session = Objects.requireNonNull(context.sender().connection()); + session.openPauseScreenAdditions(); + if (!session.hasFormOpen()) { + context.sender().sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.options.fail", session.locale())); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/QuickActionsCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/QuickActionsCommand.java new file mode 100644 index 000000000..07f76e62a --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/command/defaults/QuickActionsCommand.java @@ -0,0 +1,51 @@ +/* + * 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.command.defaults; + +import org.geysermc.geyser.api.util.TriState; +import org.geysermc.geyser.command.GeyserCommand; +import org.geysermc.geyser.command.GeyserCommandSource; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.text.GeyserLocale; +import org.incendo.cloud.context.CommandContext; + +import java.util.Objects; + +public class QuickActionsCommand extends GeyserCommand { + + public QuickActionsCommand(String name, String description, String permission) { + super(name, description, permission, TriState.TRUE, true, true); + } + + @Override + public void execute(CommandContext context) { + GeyserSession session = Objects.requireNonNull(context.sender().connection()); + session.openQuickActions(); + if (!session.hasFormOpen()) { + context.sender().sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.quickactions.fail", session.locale())); + } + } +} 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 77030306c..568ee351e 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java +++ b/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java @@ -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.HangingEntity; import org.geysermc.geyser.entity.type.ThrowableEggEntity; import org.geysermc.geyser.entity.type.EnderCrystalEntity; import org.geysermc.geyser.entity.type.EnderEyeEntity; @@ -81,6 +82,7 @@ import org.geysermc.geyser.entity.type.living.TadpoleEntity; import org.geysermc.geyser.entity.type.living.animal.ArmadilloEntity; import org.geysermc.geyser.entity.type.living.animal.AxolotlEntity; import org.geysermc.geyser.entity.type.living.animal.BeeEntity; +import org.geysermc.geyser.entity.type.living.animal.HappyGhastEntity; import org.geysermc.geyser.entity.type.living.animal.farm.ChickenEntity; import org.geysermc.geyser.entity.type.living.animal.farm.CowEntity; import org.geysermc.geyser.entity.type.living.animal.FoxEntity; @@ -215,6 +217,7 @@ public final class EntityDefinitions { public static final EntityDefinition GLOW_SQUID; public static final EntityDefinition GOAT; public static final EntityDefinition GUARDIAN; + public static final EntityDefinition HAPPY_GHAST; public static final EntityDefinition HOGLIN; public static final EntityDefinition HOPPER_MINECART; public static final EntityDefinition HORSE; @@ -395,10 +398,6 @@ public final class EntityDefinitions { .type(EntityType.LLAMA_SPIT) .heightAndWidth(0.25f) .build(); - PAINTING = EntityDefinition.inherited(null, entityBase) - .type(EntityType.PAINTING) - .addTranslator(MetadataTypes.PAINTING_VARIANT, PaintingEntity::setPaintingType) - .build(); SHULKER_BULLET = EntityDefinition.inherited(ThrowableEntity::new, entityBase) .type(EntityType.SHULKER_BULLET) .heightAndWidth(0.3125f) @@ -525,8 +524,17 @@ public final class EntityDefinitions { .addTranslator(MetadataTypes.BOOLEAN, (tridentEntity, entityMetadata) -> tridentEntity.setFlag(EntityFlag.ENCHANTED, ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue())) .build(); + EntityDefinition hangingEntityBase = EntityDefinition.inherited(null, entityBase) + .addTranslator(MetadataTypes.DIRECTION, HangingEntity::setDirectionMetadata) + .build(); + + PAINTING = EntityDefinition.inherited(PaintingEntity::new, hangingEntityBase) + .type(EntityType.PAINTING) + .addTranslator(MetadataTypes.PAINTING_VARIANT, PaintingEntity::setPaintingType) + .build(); + // Item frames are handled differently as they are blocks, not items, in Bedrock - ITEM_FRAME = EntityDefinition.inherited(null, entityBase) + ITEM_FRAME = EntityDefinition.inherited(ItemFrameEntity::new, hangingEntityBase) .type(EntityType.ITEM_FRAME) .addTranslator(MetadataTypes.ITEM_STACK, ItemFrameEntity::setItemInFrame) .addTranslator(MetadataTypes.INT, ItemFrameEntity::setItemRotation) @@ -989,6 +997,13 @@ public final class EntityDefinitions { .addTranslator(MetadataTypes.FROG_VARIANT, FrogEntity::setVariant) .addTranslator(MetadataTypes.OPTIONAL_UNSIGNED_INT, FrogEntity::setTongueTarget) .build(); + HAPPY_GHAST = EntityDefinition.inherited(HappyGhastEntity::new, ageableEntityBase) + .type(EntityType.HAPPY_GHAST) + .heightAndWidth(4f) + .properties(VanillaEntityProperties.HAPPY_GHAST) + .addTranslator(null) // Is leash holder + .addTranslator(MetadataTypes.BOOLEAN, HappyGhastEntity::setStaysStill) + .build(); HOGLIN = EntityDefinition.inherited(HoglinEntity::new, ageableEntityBase) .type(EntityType.HOGLIN) .height(1.4f).width(1.3965f) 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 index 305dbf22e..06a715f18 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/properties/VanillaEntityProperties.java +++ b/core/src/main/java/org/geysermc/geyser/entity/properties/VanillaEntityProperties.java @@ -61,6 +61,10 @@ public class VanillaEntityProperties { .addInt(CreakingEntity.CREAKING_SWAYING_TICKS, 0, 6) .build(); + public static final GeyserEntityProperties HAPPY_GHAST = new GeyserEntityProperties.Builder() + .addBoolean("minecraft:can_move") + .build(); + public static final GeyserEntityProperties WOLF_SOUND_VARIANT = new GeyserEntityProperties.Builder() .addEnum("minecraft:sound_variant", "default", 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 a5c583b96..0ba1c1277 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 @@ -87,10 +87,8 @@ public class BoatEntity extends Entity implements Leashable, Tickable { @Override protected void initializeMetadata() { super.initializeMetadata(); - if (GameProtocol.is1_21_70orHigher(session)) { - // Without this flag you cant stand on boats - setFlag(EntityFlag.COLLIDABLE, true); - } + // 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/DisplayBaseEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/DisplayBaseEntity.java index 16587d125..414ed0541 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/DisplayBaseEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/DisplayBaseEntity.java @@ -66,7 +66,7 @@ public class DisplayBaseEntity extends Entity { this.setRiderSeatPosition(this.baseTranslation); this.moveRelative(this.baseTranslation.getX(), this.baseTranslation.getY(), this.baseTranslation.getZ(), yaw, pitch, headYaw, false); } else { - EntityUtils.updateMountOffset(this, this.vehicle, true, true, false); + EntityUtils.updateMountOffset(this, this.vehicle, true, true, 0, 1); this.updateBedrockMetadata(); } } 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 581a6d319..b03dea3ed 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 @@ -45,7 +45,11 @@ import org.geysermc.geyser.api.entity.type.GeyserEntity; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.entity.GeyserDirtyMetadata; import org.geysermc.geyser.entity.properties.GeyserEntityPropertyManager; +import org.geysermc.geyser.entity.type.living.MobEntity; +import org.geysermc.geyser.entity.type.player.PlayerEntity; import org.geysermc.geyser.item.Items; +import org.geysermc.geyser.item.type.Item; +import org.geysermc.geyser.level.physics.BoundingBox; import org.geysermc.geyser.scoreboard.Team; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.text.MessageTranslator; @@ -617,7 +621,7 @@ public class Entity implements GeyserEntity { Entity passenger = passengers.get(i); if (passenger != null) { boolean rider = i == 0; - EntityUtils.updateMountOffset(passenger, this, rider, true, passengers.size() > 1); + EntityUtils.updateMountOffset(passenger, this, rider, true, i, passengers.size()); passenger.updateBedrockMetadata(); } } @@ -629,7 +633,7 @@ public class Entity implements GeyserEntity { protected void updateMountOffset() { if (vehicle != null) { boolean rider = vehicle.getPassengers().get(0) == this; - EntityUtils.updateMountOffset(this, vehicle, rider, true, vehicle.getPassengers().size() > 1); + EntityUtils.updateMountOffset(this, vehicle, rider, true, vehicle.getPassengers().indexOf(this), vehicle.getPassengers().size()); updateBedrockMetadata(); } } @@ -682,13 +686,23 @@ public class Entity implements GeyserEntity { * to ensure packet parity as well as functionality parity (such as sound effect responses). */ public InteractionResult interact(Hand hand) { - if (isAlive() && this instanceof Leashable leashable) { + Item itemInHand = session.getPlayerInventory().getItemInHand(hand).asItem(); + if (itemInHand == Items.SHEARS) { + if (hasLeashesToDrop()) { + return InteractionResult.SUCCESS; + } + + if (this instanceof MobEntity mob && !session.isSneaking() && mob.canShearEquipment()) { + return InteractionResult.SUCCESS; + } + } else if (isAlive() && this instanceof Leashable leashable) { if (leashable.leashHolderBedrockId() == session.getPlayerEntity().getGeyserId()) { // Note this might also update client side (a theoretical Geyser/client desync and Java parity issue). // Has yet to be an issue though, as of Java 1.21. return InteractionResult.SUCCESS; } - if (session.getPlayerInventory().getItemInHand(hand).asItem() == Items.LEAD && leashable.canBeLeashed()) { + if (session.getPlayerInventory().getItemInHand(hand).asItem() == Items.LEAD + && !(session.getEntityCache().getEntityByGeyserId(leashable.leashHolderBedrockId()) instanceof PlayerEntity)) { // We shall leash return InteractionResult.SUCCESS; } @@ -697,6 +711,23 @@ public class Entity implements GeyserEntity { return InteractionResult.PASS; } + public boolean hasLeashesToDrop() { + BoundingBox searchBB = new BoundingBox(position.getX(), position.getY(), position.getZ(), 32, 32, 32); + List leashedInRange = session.getEntityCache().getEntities().values().stream() + .filter(entity -> entity instanceof Leashable leashablex && leashablex.leashHolderBedrockId() == this.getGeyserId()) + .filter(entity -> { + BoundingBox leashedBB = new BoundingBox(entity.position.toDouble(), entity.boundingBoxWidth, entity.boundingBoxHeight, entity.boundingBoxWidth); + return searchBB.checkIntersection(leashedBB); + }).map(Leashable.class::cast).toList(); + + boolean found = !leashedInRange.isEmpty(); + if (this instanceof Leashable leashable && leashable.isLeashed()) { + found = true; + } + + return found; + } + /** * Simulates interacting with this entity at a specific click point. As of Java Edition 1.18.1, this is only used for armor stands. */ diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/HangingEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/HangingEntity.java new file mode 100644 index 000000000..7c0e8a773 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/type/HangingEntity.java @@ -0,0 +1,47 @@ +/* + * 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 org.cloudburstmc.math.vector.Vector3f; +import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.mcprotocollib.protocol.data.game.entity.object.Direction; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata; + +import java.util.UUID; + +public abstract class HangingEntity extends Entity { + + public HangingEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { + super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); + } + + public void setDirectionMetadata(EntityMetadata direction) { + setDirection(direction.getValue()); + } + + public abstract void setDirection(Direction direction); +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/ItemFrameEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/ItemFrameEntity.java index 680bdecd8..8208f7525 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/ItemFrameEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/ItemFrameEntity.java @@ -51,7 +51,7 @@ import java.util.UUID; /** * Item frames are an entity in Java but a block entity in Bedrock. */ -public class ItemFrameEntity extends Entity { +public class ItemFrameEntity extends HangingEntity { /** * Used for getting the Bedrock block position. * Blocks deal with integers whereas entities deal with floats. @@ -60,7 +60,7 @@ public class ItemFrameEntity extends Entity { /** * Specific block 'state' we are emulating in Bedrock. */ - private final BlockDefinition blockDefinition; + private BlockDefinition blockDefinition; /** * Rotation of item in frame. */ @@ -75,22 +75,14 @@ public class ItemFrameEntity extends Entity { @Getter private ItemStack heldItem = null; /** - * Determines if this entity needs updated on the client end/ + * Determines if this entity needs to be updated on the client end. */ private boolean changed = true; - public ItemFrameEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw, Direction direction) { + public ItemFrameEntity(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); - NbtMapBuilder blockBuilder = NbtMap.builder() - .putString("name", this.definition.entityType() == EntityType.GLOW_ITEM_FRAME ? "minecraft:glow_frame" : "minecraft:frame"); - NbtMapBuilder statesBuilder = NbtMap.builder() - .putInt("facing_direction", direction.ordinal()) - .putByte("item_frame_map_bit", (byte) 0) - .putByte("item_frame_photo_bit", (byte) 0); - blockBuilder.put("states", statesBuilder.build()); - - blockDefinition = session.getBlockMappings().getItemFrame(blockBuilder.build()); + blockDefinition = buildBlockDefinition(Direction.SOUTH); // Default to SOUTH direction, like on Java - entity metadata should correct this when necessary bedrockPosition = Vector3i.from(position.getFloorX(), position.getFloorY(), position.getFloorZ()); session.getItemFrameCache().put(bedrockPosition, this); @@ -109,6 +101,12 @@ public class ItemFrameEntity extends Entity { valid = true; } + @Override + public void setDirection(Direction direction) { + blockDefinition = buildBlockDefinition(direction); + changed = true; + } + public void setItemInFrame(EntityMetadata entityMetadata) { if (entityMetadata.getValue() != null) { this.heldItem = entityMetadata.getValue(); @@ -222,6 +220,18 @@ public class ItemFrameEntity extends Entity { return InventoryUtils.isEmpty(heldItem) && session.getPlayerInventory().getItemInHand(hand).isEmpty() ? InteractionResult.PASS : InteractionResult.SUCCESS; } + private BlockDefinition buildBlockDefinition(Direction direction) { + NbtMapBuilder blockBuilder = NbtMap.builder() + .putString("name", this.definition.entityType() == EntityType.GLOW_ITEM_FRAME ? "minecraft:glow_frame" : "minecraft:frame"); + NbtMapBuilder statesBuilder = NbtMap.builder() + .putInt("facing_direction", direction.ordinal()) + .putByte("item_frame_map_bit", (byte) 0) + .putByte("item_frame_photo_bit", (byte) 0); + blockBuilder.put("states", statesBuilder.build()); + + return session.getBlockMappings().getItemFrame(blockBuilder.build()); + } + /** * Finds the Java entity ID of an item frame from its Bedrock position. * @param position position of item frame in Bedrock. diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/Leashable.java b/core/src/main/java/org/geysermc/geyser/entity/type/Leashable.java index 64d95ba3c..24527c5e7 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/Leashable.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/Leashable.java @@ -35,10 +35,10 @@ public interface Leashable { long leashHolderBedrockId(); default boolean canBeLeashed() { - return isNotLeashed(); + return true; } - default boolean isNotLeashed() { - return leashHolderBedrockId() == -1L; + default boolean isLeashed() { + return leashHolderBedrockId() != -1L; } } 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 f4a788518..92048eb25 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 @@ -41,16 +41,17 @@ import org.cloudburstmc.protocol.bedrock.packet.MobEquipmentPacket; import org.cloudburstmc.protocol.bedrock.packet.UpdateAttributesPacket; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.entity.attribute.GeyserAttributeType; +import org.geysermc.geyser.entity.type.living.animal.HappyGhastEntity; import org.geysermc.geyser.entity.vehicle.ClientVehicle; +import org.geysermc.geyser.entity.vehicle.HappyGhastVehicleComponent; import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.item.Items; -import org.geysermc.geyser.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; import org.geysermc.geyser.translator.item.ItemTranslator; import org.geysermc.geyser.util.AttributeUtils; +import org.geysermc.geyser.util.EntityUtils; import org.geysermc.geyser.util.InteractionResult; import org.geysermc.geyser.util.MathUtils; import org.geysermc.mcprotocollib.protocol.data.game.entity.EquipmentSlot; @@ -63,9 +64,7 @@ import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.FloatE 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.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.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; @@ -73,6 +72,7 @@ import org.geysermc.mcprotocollib.protocol.data.game.level.particle.ParticleType import java.util.ArrayList; import java.util.Collections; +import java.util.EnumMap; import java.util.List; import java.util.Optional; import java.util.UUID; @@ -80,6 +80,8 @@ import java.util.UUID; @Getter @Setter public class LivingEntity extends Entity { + protected EnumMap equipment = new EnumMap<>(EquipmentSlot.class); + protected ItemData helmet = ItemData.AIR; protected ItemData chestplate = ItemData.AIR; protected ItemData leggings = ItemData.AIR; @@ -116,47 +118,51 @@ public class LivingEntity extends Entity { super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); } - public void setHelmet(ItemStack stack) { + public void setHelmet(GeyserItemStack stack) { + this.equipment.put(EquipmentSlot.HELMET, stack); this.helmet = ItemTranslator.translateToBedrock(session, stack); } - public void setChestplate(ItemStack stack) { + public void setChestplate(GeyserItemStack stack) { + this.equipment.put(EquipmentSlot.CHESTPLATE, stack); this.chestplate = ItemTranslator.translateToBedrock(session, stack); } - public void setLeggings(ItemStack stack) { + public void setLeggings(GeyserItemStack stack) { + this.equipment.put(EquipmentSlot.LEGGINGS, stack); this.leggings = ItemTranslator.translateToBedrock(session, stack); } - public void setBoots(ItemStack stack) { + public void setBoots(GeyserItemStack stack) { + this.equipment.put(EquipmentSlot.BOOTS, stack); this.boots = ItemTranslator.translateToBedrock(session, stack); } - public void setBody(ItemStack stack) { + public void setBody(GeyserItemStack stack) { + this.equipment.put(EquipmentSlot.BODY, stack); this.body = ItemTranslator.translateToBedrock(session, stack); } - public void setSaddle(@Nullable ItemStack stack) { + public void setSaddle(GeyserItemStack stack) { + this.equipment.put(EquipmentSlot.SADDLE, 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; - } + if (!stack.isEmpty()) { + Equippable equippable = stack.getComponent(DataComponentTypes.EQUIPPABLE); + saddled = equippable != null && equippable.slot() == EquipmentSlot.SADDLE; } updateSaddled(saddled); } - public void setHand(ItemStack stack) { + public void setHand(GeyserItemStack stack) { + this.equipment.put(EquipmentSlot.MAIN_HAND, stack); this.hand = ItemTranslator.translateToBedrock(session, stack); } - public void setOffhand(ItemStack stack) { + public void setOffhand(GeyserItemStack stack) { + this.equipment.put(EquipmentSlot.OFF_HAND, stack); this.offhand = ItemTranslator.translateToBedrock(session, stack); } @@ -172,9 +178,13 @@ public class LivingEntity extends Entity { } public void switchHands() { - ItemData offhand = this.offhand; + GeyserItemStack javaOffhand = this.equipment.get(EquipmentSlot.OFF_HAND); + this.equipment.put(EquipmentSlot.OFF_HAND, this.equipment.get(EquipmentSlot.MAIN_HAND)); + this.equipment.put(EquipmentSlot.MAIN_HAND, javaOffhand); + + ItemData bedrockOffhand = this.offhand; this.offhand = this.hand; - this.hand = offhand; + this.hand = bedrockOffhand; } @Override @@ -503,7 +513,13 @@ public class LivingEntity extends Entity { } } case ATTACK_DAMAGE -> newAttributes.add(calculateAttribute(javaAttribute, GeyserAttributeType.ATTACK_DAMAGE)); - case FLYING_SPEED -> newAttributes.add(calculateAttribute(javaAttribute, GeyserAttributeType.FLYING_SPEED)); + case FLYING_SPEED -> { + AttributeData attributeData = calculateAttribute(javaAttribute, GeyserAttributeType.FLYING_SPEED); + newAttributes.add(attributeData); + if (this instanceof HappyGhastEntity ghast && ghast.getVehicleComponent() instanceof HappyGhastVehicleComponent component) { + component.setFlyingSpeed(attributeData.getValue()); + } + } case FOLLOW_RANGE -> newAttributes.add(calculateAttribute(javaAttribute, GeyserAttributeType.FOLLOW_RANGE)); case KNOCKBACK_RESISTANCE -> newAttributes.add(calculateAttribute(javaAttribute, GeyserAttributeType.KNOCKBACK_RESISTANCE)); case JUMP_STRENGTH -> newAttributes.add(calculateAttribute(javaAttribute, GeyserAttributeType.HORSE_JUMP_STRENGTH)); @@ -512,10 +528,41 @@ public class LivingEntity extends Entity { setAttributeScale((float) AttributeUtils.calculateValue(javaAttribute)); updateBedrockMetadata(); } + case WATER_MOVEMENT_EFFICIENCY -> { + if (this instanceof ClientVehicle clientVehicle) { + clientVehicle.getVehicleComponent().setWaterMovementEfficiency(AttributeUtils.calculateValue(javaAttribute)); + } + } } } } + protected boolean hasBodyArmor() { + return this.hasValidEquippableItemForSlot(EquipmentSlot.BODY); + } + + private boolean hasValidEquippableItemForSlot(EquipmentSlot slot) { + // MojMap LivingEntity#hasItemInSlot + GeyserItemStack itemInSlot = equipment.get(slot); + if (itemInSlot != null) { + // MojMap LivingEntity#isEquippableInSlot + Equippable equippable = itemInSlot.getComponent(DataComponentTypes.EQUIPPABLE); + if (equippable != null) { + return slot == equippable.slot() && + canUseSlot(slot) && + EntityUtils.equipmentUsableByEntity(session, equippable, this.definition.entityType()); + } else { + return slot == EquipmentSlot.MAIN_HAND && canUseSlot(EquipmentSlot.MAIN_HAND); + } + } + + return false; + } + + protected boolean canUseSlot(EquipmentSlot slot) { + return true; + } + /** * Calculates the complete attribute value to send to Bedrock. Will be overriden if attributes need to be cached. */ 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 b7e900365..fb89a884d 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 @@ -38,13 +38,13 @@ import org.geysermc.mcprotocollib.protocol.data.game.entity.object.Direction; import java.util.UUID; -public class PaintingEntity extends Entity { +public class PaintingEntity extends HangingEntity { private static final double OFFSET = -0.46875; - private final Direction direction; + private int paintingId = -1; // Ideally this would be the default painting Java uses in their metadata, but seems to depend on the current paintings loaded in the registry + private Direction direction = Direction.SOUTH; // Default to SOUTH direction, like on Java - entity metadata should correct this when necessary - public PaintingEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw, Direction direction) { + public PaintingEntity(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); - this.direction = direction; } @Override @@ -52,11 +52,31 @@ public class PaintingEntity extends Entity { // Wait until we get the metadata needed } + @Override + public void setDirection(Direction direction) { + this.direction = direction; + updatePainting(); + } + public void setPaintingType(ObjectEntityMetadata> entityMetadata) { if (!entityMetadata.getValue().isId()) { return; } - PaintingType type = session.getRegistryCache().registry(JavaRegistries.PAINTING_VARIANT).byId(entityMetadata.getValue().id()); + paintingId = entityMetadata.getValue().id(); + updatePainting(); + } + + private void updatePainting() { + if (paintingId == -1) { + return; + } else if (valid) { + despawnEntity(); + } + + PaintingType type = session.getRegistryCache().registry(JavaRegistries.PAINTING_VARIANT).byId(paintingId); + if (type == null) { + return; + } AddPaintingPacket addPaintingPacket = new AddPaintingPacket(); addPaintingPacket.setUniqueEntityId(geyserId); addPaintingPacket.setRuntimeEntityId(geyserId); diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/ThrowableEggEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/ThrowableEggEntity.java index 92445cabd..0c7b5ea20 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/ThrowableEggEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/ThrowableEggEntity.java @@ -70,7 +70,7 @@ public class ThrowableEggEntity extends ThrowableItemEntity { 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)); + Key chickenVariant = holder.getOrCompute(id -> JavaRegistries.CHICKEN_VARIANT.key(session, id)); for (var variant : TemperatureVariantAnimal.BuiltInVariant.values()) { if (chickenVariant.asMinimalString().equalsIgnoreCase(variant.name())) { return chickenVariant.asMinimalString().toLowerCase(Locale.ROOT); diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/ArmorStandEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/ArmorStandEntity.java index e1c82345f..afbdcca31 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/ArmorStandEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/ArmorStandEntity.java @@ -36,6 +36,7 @@ import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData; import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.entity.EntityDefinitions; import org.geysermc.geyser.entity.type.LivingEntity; +import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.item.Items; import org.geysermc.geyser.scoreboard.Team; import org.geysermc.geyser.session.GeyserSession; @@ -45,7 +46,6 @@ import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetad import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ByteEntityMetadata; import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; -import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack; import java.util.Optional; import java.util.UUID; @@ -266,37 +266,37 @@ public class ArmorStandEntity extends LivingEntity { } @Override - public void setHelmet(ItemStack helmet) { + public void setHelmet(GeyserItemStack helmet) { super.setHelmet(helmet); updateSecondEntityStatus(true); } @Override - public void setChestplate(ItemStack chestplate) { + public void setChestplate(GeyserItemStack chestplate) { super.setChestplate(chestplate); updateSecondEntityStatus(true); } @Override - public void setLeggings(ItemStack leggings) { + public void setLeggings(GeyserItemStack leggings) { super.setLeggings(leggings); updateSecondEntityStatus(true); } @Override - public void setBoots(ItemStack boots) { + public void setBoots(GeyserItemStack boots) { super.setBoots(boots); updateSecondEntityStatus(true); } @Override - public void setHand(ItemStack hand) { + public void setHand(GeyserItemStack hand) { super.setHand(hand); updateSecondEntityStatus(true); } @Override - public void setOffhand(ItemStack offHand) { + public void setOffhand(GeyserItemStack offHand) { super.setOffhand(offHand); updateSecondEntityStatus(true); } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/MobEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/MobEntity.java index 9accf178f..558bffd99 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/MobEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/MobEntity.java @@ -34,12 +34,18 @@ import org.geysermc.geyser.entity.type.Leashable; import org.geysermc.geyser.entity.type.LivingEntity; 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.SpawnEggItem; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.util.InteractionResult; import org.geysermc.geyser.util.InteractiveTag; +import org.geysermc.geyser.util.ItemUtils; +import org.geysermc.mcprotocollib.protocol.data.game.entity.EquipmentSlot; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ByteEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.GameMode; import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentTypes; +import org.geysermc.mcprotocollib.protocol.data.game.item.component.Equippable; import java.util.UUID; @@ -108,6 +114,26 @@ public class MobEntity extends LivingEntity implements Leashable { } } + public boolean canShearEquipment() { + if (!passengers.isEmpty()) { + return false; + } + + for (EquipmentSlot slot : EquipmentSlot.values()) { + GeyserItemStack equipped = equipment.get(slot); + if (equipped == null || equipped.isEmpty()) continue; + + Equippable equippable = equipped.getComponent(DataComponentTypes.EQUIPPABLE); + if (equippable != null && equippable.canBeSheared()) { + if (!ItemUtils.hasEffect(session, equipped, EnchantmentComponent.PREVENT_ARMOR_CHANGE) || session.getGameMode() == GameMode.CREATIVE) { + return true; + } + } + } + + return false; + } + private InteractionResult checkPriorityInteractions(GeyserItemStack itemInHand) { if (itemInHand.asItem() == Items.NAME_TAG) { InteractionResult result = checkInteractWithNameTag(itemInHand); @@ -136,7 +162,7 @@ public class MobEntity extends LivingEntity implements Leashable { @Override public boolean canBeLeashed() { - return isNotLeashed() && !isEnemy(); + return !isEnemy(); } @Override diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/SquidEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/SquidEntity.java index ac3456829..fe3e26a6c 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/SquidEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/SquidEntity.java @@ -123,7 +123,7 @@ public class SquidEntity extends AgeableWaterEntity implements Tickable { @Override public boolean canBeLeashed() { - return isNotLeashed(); + return true; } private void checkInWater() { diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/HappyGhastEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/HappyGhastEntity.java new file mode 100644 index 000000000..6a9111f99 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/HappyGhastEntity.java @@ -0,0 +1,213 @@ +/* + * 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 org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.cloudburstmc.math.TrigMath; +import org.cloudburstmc.math.vector.Vector2f; +import org.cloudburstmc.math.vector.Vector3f; +import org.cloudburstmc.protocol.bedrock.data.AttributeData; +import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; +import org.geysermc.geyser.entity.EntityDefinition; +import org.geysermc.geyser.entity.type.Entity; +import org.geysermc.geyser.entity.type.player.SessionPlayerEntity; +import org.geysermc.geyser.entity.vehicle.ClientVehicle; +import org.geysermc.geyser.entity.vehicle.HappyGhastVehicleComponent; +import org.geysermc.geyser.entity.vehicle.VehicleComponent; +import org.geysermc.geyser.inventory.GeyserItemStack; +import org.geysermc.geyser.item.type.Item; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.session.cache.tags.ItemTag; +import org.geysermc.geyser.session.cache.tags.Tag; +import org.geysermc.geyser.util.AttributeUtils; +import org.geysermc.geyser.util.InteractionResult; +import org.geysermc.geyser.util.InteractiveTag; +import org.geysermc.mcprotocollib.protocol.data.game.entity.EquipmentSlot; +import org.geysermc.mcprotocollib.protocol.data.game.entity.attribute.Attribute; +import org.geysermc.mcprotocollib.protocol.data.game.entity.attribute.AttributeType; +import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; +import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; + +import java.util.List; +import java.util.UUID; + +public class HappyGhastEntity extends AnimalEntity implements ClientVehicle { + + public static final float[] X_OFFSETS = {0.0F, -1.7F, 0.0F, 1.7F}; + public static final float[] Z_OFFSETS = {1.7F, 0.0F, -1.7F, 0.0F}; + + private final HappyGhastVehicleComponent vehicleComponent = new HappyGhastVehicleComponent(this, 0.0f); + private boolean staysStill; + + public HappyGhastEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { + super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); + } + + @Override + protected void initializeMetadata() { + super.initializeMetadata(); + // BDS 1.21.90 + setFlag(EntityFlag.CAN_FLY, true); + setFlag(EntityFlag.CAN_WALK, true); + setFlag(EntityFlag.TAMED, true); + setFlag(EntityFlag.BODY_ROTATION_ALWAYS_FOLLOWS_HEAD, true); + setFlag(EntityFlag.COLLIDABLE, true); + + setFlag(EntityFlag.WASD_AIR_CONTROLLED, true); + setFlag(EntityFlag.DOES_SERVER_AUTH_ONLY_DISMOUNT, true); + + propertyManager.add("minecraft:can_move", true); + } + + @Override + @Nullable + protected Tag getFoodTag() { + return ItemTag.HAPPY_GHAST_FOOD; + } + + @Override + protected float getBabySize() { + return 0.2375f; + } + + public void setStaysStill(BooleanEntityMetadata entityMetadata) { + staysStill = entityMetadata.getPrimitiveValue(); + propertyManager.add("minecraft:can_move", !entityMetadata.getPrimitiveValue()); + updateBedrockEntityProperties(); + } + + @NonNull + @Override + protected InteractiveTag testMobInteraction(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) { + if (this.isBaby()) { + return super.testMobInteraction(hand, itemInHand); + } else { + if (!itemInHand.isEmpty()) { + if (session.getTagCache().is(ItemTag.HARNESSES, itemInHand)) { + if (this.equipment.get(EquipmentSlot.BODY) == null) { + // Harnesses the ghast + return InteractiveTag.EQUIP_HARNESS; + } + } + // TODO: Handle shearing the harness off + } + + if (this.equipment.get(EquipmentSlot.BODY) != null && !session.isSneaking()) { + // Rides happy ghast + return InteractiveTag.RIDE_HORSE; + } else { + return super.testMobInteraction(hand, itemInHand); + } + } + } + + @NonNull + @Override + protected InteractionResult mobInteract(@NonNull Hand hand, @NonNull GeyserItemStack itemInHand) { + if (this.isBaby()) { + return super.mobInteract(hand, itemInHand); + } else { + if (!itemInHand.isEmpty()) { + if (session.getTagCache().is(ItemTag.HARNESSES, itemInHand)) { + if (this.equipment.get(EquipmentSlot.BODY) == null) { + // Harnesses the ghast + return InteractionResult.SUCCESS; + } + } + // TODO: Handle shearing the harness off + } + + if (this.equipment.get(EquipmentSlot.BODY) == null && !session.isSneaking()) { + // Rides happy ghast + return InteractionResult.SUCCESS; + } else { + return super.mobInteract(hand, itemInHand); + } + } + } + + @Override + public VehicleComponent getVehicleComponent() { + return vehicleComponent; + } + + @Override + public Vector3f getRiddenInput(Vector2f input) { + float x = input.getX(); + float y = 0.0f; + float z = 0.0f; + + if (input.getY() != 0.0f) { + float pitch = session.getPlayerEntity().getPitch(); + z = TrigMath.cos(pitch * TrigMath.DEG_TO_RAD); + y = -TrigMath.sin(pitch * TrigMath.DEG_TO_RAD); + if (input.getY() < 0.0f) { + z *= -0.5f; + y *= -0.5f; + } + } + + if (session.getInputCache().wasJumping()) { + y += 0.5f; + } + + return Vector3f.from(x, y, z).mul(3.9f * vehicleComponent.getFlyingSpeed()); + } + + @Override + public float getVehicleSpeed() { + return 0.0f; // Not used + } + + @Override + public boolean isClientControlled() { + if (!hasBodyArmor() || getFlag(EntityFlag.NO_AI) || staysStill) { + return false; + } + + return getFirstPassenger() instanceof SessionPlayerEntity; + } + + private Entity getFirstPassenger() { + return passengers.isEmpty() ? null : passengers.get(0); + } + + @Override + protected void updateAttribute(Attribute javaAttribute, List newAttributes) { + super.updateAttribute(javaAttribute, newAttributes); + if (javaAttribute.getType() instanceof AttributeType.Builtin type) { + if (type == AttributeType.Builtin.CAMERA_DISTANCE) { + vehicleComponent.setCameraDistance((float) AttributeUtils.calculateValue(javaAttribute)); + } + } + } + + @Override + protected boolean canUseSlot(EquipmentSlot slot) { + return slot != EquipmentSlot.BODY ? super.canUseSlot(slot) : this.isAlive() && !this.isBaby(); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/HoglinEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/HoglinEntity.java index b506f1425..9ea70fa78 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/HoglinEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/HoglinEntity.java @@ -66,7 +66,7 @@ public class HoglinEntity extends AnimalEntity { @Override public boolean canBeLeashed() { - return isNotLeashed(); + return true; } @Override 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 236b22c51..36a126687 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 @@ -47,6 +47,7 @@ import org.geysermc.geyser.session.cache.tags.Tag; import org.geysermc.geyser.util.EntityUtils; import org.geysermc.geyser.util.InteractionResult; import org.geysermc.geyser.util.InteractiveTag; +import org.geysermc.mcprotocollib.protocol.data.game.entity.EquipmentSlot; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata; import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; @@ -169,8 +170,8 @@ public class StriderEntity extends AnimalEntity implements Tickable, ClientVehic } @Override - public Vector2f getAdjustedInput(Vector2f input) { - return Vector2f.UNIT_Y; + public Vector3f getRiddenInput(Vector2f input) { + return Vector3f.UNIT_Z; } @Override @@ -195,4 +196,9 @@ public class StriderEntity extends AnimalEntity implements Tickable, ClientVehic public boolean canWalkOnLava() { return true; } + + @Override + protected boolean canUseSlot(EquipmentSlot slot) { + return slot != EquipmentSlot.SADDLE ? super.canUseSlot(slot) : this.isAlive() && !this.isBaby(); + } } 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 index 3936ca81a..c47480563 100644 --- 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 @@ -55,7 +55,7 @@ public interface VariantHolder { * Sets the variant of the entity. */ default void setVariantFromJavaId(int variant) { - setBedrockVariant(variantRegistry().fromNetworkId(getSession(), variant)); + setBedrockVariant(variantRegistry().value(getSession(), variant)); } GeyserSession getSession(); diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/PigEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/PigEntity.java index d6a8ece7c..7ae672bcc 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/PigEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/farm/PigEntity.java @@ -48,6 +48,7 @@ import org.geysermc.geyser.session.cache.tags.Tag; import org.geysermc.geyser.util.EntityUtils; import org.geysermc.geyser.util.InteractionResult; import org.geysermc.geyser.util.InteractiveTag; +import org.geysermc.mcprotocollib.protocol.data.game.entity.EquipmentSlot; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata; import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; @@ -128,8 +129,8 @@ public class PigEntity extends TemperatureVariantAnimal implements Tickable, Cli } @Override - public Vector2f getAdjustedInput(Vector2f input) { - return Vector2f.UNIT_Y; + public Vector3f getRiddenInput(Vector2f input) { + return Vector3f.UNIT_Z; } @Override @@ -154,4 +155,9 @@ public class PigEntity extends TemperatureVariantAnimal implements Tickable, Cli public JavaRegistryKey variantRegistry() { return JavaRegistries.PIG_VARIANT; } + + @Override + protected boolean canUseSlot(EquipmentSlot slot) { + return slot != EquipmentSlot.SADDLE ? super.canUseSlot(slot) : this.isAlive() && !this.isBaby(); + } } 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 7b6184579..8b0e77c73 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 @@ -45,6 +45,7 @@ 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.mcprotocollib.protocol.data.game.entity.EquipmentSlot; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ByteEntityMetadata; import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; @@ -286,4 +287,13 @@ public class AbstractHorseEntity extends AnimalEntity { return InteractionResult.SUCCESS; } } + + @Override + protected boolean canUseSlot(EquipmentSlot slot) { + if (slot != EquipmentSlot.SADDLE) { + return super.canUseSlot(slot); + } else { + return isAlive() && !isBaby() && getFlag(EntityFlag.TAMED); + } + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/CamelEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/CamelEntity.java index 6239122f4..138b60e35 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/CamelEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/CamelEntity.java @@ -68,8 +68,6 @@ public class CamelEntity extends AbstractHorseEntity implements ClientVehicle { public void setHorseFlags(ByteEntityMetadata entityMetadata) { byte xd = entityMetadata.getPrimitiveValue(); - boolean saddled = (xd & 0x04) == 0x04; - setFlag(EntityFlag.SADDLED, saddled); setFlag(EntityFlag.EATING, (xd & 0x10) == 0x10); setFlag(EntityFlag.STANDING, (xd & 0x20) == 0x20); @@ -98,7 +96,7 @@ public class CamelEntity extends AbstractHorseEntity implements ClientVehicle { } // Shows the dash meter - setFlag(EntityFlag.CAN_DASH, saddled); + // setFlag(EntityFlag.CAN_DASH, saddled); } @Override @@ -155,8 +153,9 @@ public class CamelEntity extends AbstractHorseEntity implements ClientVehicle { } @Override - public Vector2f getAdjustedInput(Vector2f input) { - return input.mul(0.5f, input.getY() < 0 ? 0.25f : 1.0f); + public Vector3f getRiddenInput(Vector2f input) { + input = input.mul(0.5f, input.getY() < 0 ? 0.25f : 1.0f); + return Vector3f.from(input.getX(), 0.0, input.getY()); } @Override diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/HorseEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/HorseEntity.java index b8a9a8f28..478f89aeb 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/HorseEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/HorseEntity.java @@ -29,6 +29,7 @@ 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.EquipmentSlot; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata; import java.util.UUID; @@ -44,4 +45,9 @@ public class HorseEntity extends AbstractHorseEntity { dirtyMetadata.put(EntityDataTypes.VARIANT, value & 255); dirtyMetadata.put(EntityDataTypes.MARK_VARIANT, (value >> 8) % 5); } -} \ No newline at end of file + + @Override + protected boolean canUseSlot(EquipmentSlot slot) { + return true; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/LlamaEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/LlamaEntity.java index d27a1fff3..6ef1cd57a 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/LlamaEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/LlamaEntity.java @@ -35,6 +35,7 @@ import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.cache.tags.ItemTag; import org.geysermc.geyser.session.cache.tags.Tag; import org.geysermc.geyser.util.MathUtils; +import org.geysermc.mcprotocollib.protocol.data.game.entity.EquipmentSlot; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata; import java.util.UUID; @@ -61,4 +62,9 @@ public class LlamaEntity extends ChestedHorseEntity { protected @Nullable Tag getFoodTag() { return ItemTag.LLAMA_FOOD; } + + @Override + protected boolean canUseSlot(EquipmentSlot slot) { + return true; + } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/TameableEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/TameableEntity.java index ea347d193..a24eb1755 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/TameableEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/tameable/TameableEntity.java @@ -85,6 +85,6 @@ public abstract class TameableEntity extends AnimalEntity { @Override public boolean canBeLeashed() { - return isNotLeashed(); + return true; } } 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 02edfec3f..46911480e 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 @@ -51,7 +51,6 @@ import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ByteEn import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata; import org.geysermc.mcprotocollib.protocol.data.game.entity.player.GameMode; import org.geysermc.mcprotocollib.protocol.data.game.entity.player.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.item.component.HolderSet; @@ -130,16 +129,15 @@ public class WolfEntity extends TameableEntity implements VariantIntHolder { } @Override - public void setBody(ItemStack stack) { + public void setBody(GeyserItemStack stack) { super.setBody(stack); isCurseOfBinding = ItemUtils.hasEffect(session, stack, EnchantmentComponent.PREVENT_ARMOR_CHANGE); - // Not using ItemStack#getDataComponents as that wouldn't include default item components - repairableItems = GeyserItemStack.from(stack).getComponent(DataComponentTypes.REPAIRABLE); + repairableItems = stack.getComponent(DataComponentTypes.REPAIRABLE); } @Override public boolean canBeLeashed() { - return !getFlag(EntityFlag.ANGRY) && super.canBeLeashed(); + return !getFlag(EntityFlag.ANGRY); } @NonNull diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/PiglinEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/PiglinEntity.java index 19b6d8e69..36c412ba7 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/PiglinEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/PiglinEntity.java @@ -42,7 +42,6 @@ import org.geysermc.geyser.util.InteractionResult; import org.geysermc.geyser.util.InteractiveTag; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; -import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack; import java.util.UUID; @@ -71,9 +70,9 @@ public class PiglinEntity extends BasePiglinEntity { } @Override - public void setHand(ItemStack stack) { + public void setHand(GeyserItemStack stack) { ItemMapping crossbow = session.getItemMappings().getStoredItems().crossbow(); - boolean toCrossbow = stack != null && stack.getId() == crossbow.getJavaItem().javaId(); + boolean toCrossbow = stack != null && stack.asItem() == crossbow.getJavaItem(); if (toCrossbow ^ this.hand.getDefinition() == crossbow.getBedrockDefinition()) { // If switching to/from crossbow dirtyMetadata.put(EntityDataTypes.BLOCK, session.getBlockMappings().getDefinition(toCrossbow ? 0 : 1)); diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/ZoglinEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/ZoglinEntity.java index 3d6e381c7..e382777ff 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/ZoglinEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/monster/ZoglinEntity.java @@ -59,7 +59,7 @@ public class ZoglinEntity extends MonsterEntity { @Override public boolean canBeLeashed() { - return isNotLeashed(); + return true; } @Override diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/player/PlayerEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/player/PlayerEntity.java index dd48ca739..742f550d0 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/player/PlayerEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/player/PlayerEntity.java @@ -103,6 +103,11 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity { */ private @Nullable ParrotEntity rightParrot; + /** + * Whether this player is currently listed. + */ + private boolean listed = false; + public PlayerEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw, String username, @Nullable String texturesProperty) { super(session, entityId, geyserId, uuid, EntityDefinitions.PLAYER, position, motion, yaw, pitch, headYaw); @@ -156,6 +161,8 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity { // Since we re-use player entities: Clear flags, held item, etc this.resetMetadata(); this.nametag = username; + + this.equipment.clear(); this.hand = ItemData.AIR; this.offhand = ItemData.AIR; this.boots = ItemData.AIR; 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 c2b45a536..1fd4f5cf5 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 @@ -223,9 +223,10 @@ public class SessionPlayerEntity extends PlayerEntity { @Override protected void setSneaking(boolean value) { if (value) { - session.startSneaking(); + session.startSneaking(false); } else { - session.stopSneaking(); + session.setShouldSendSneak(false); + session.stopSneaking(false); } } diff --git a/core/src/main/java/org/geysermc/geyser/entity/vehicle/CamelVehicleComponent.java b/core/src/main/java/org/geysermc/geyser/entity/vehicle/CamelVehicleComponent.java index 7d022ed7c..1ed9328f0 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/vehicle/CamelVehicleComponent.java +++ b/core/src/main/java/org/geysermc/geyser/entity/vehicle/CamelVehicleComponent.java @@ -90,13 +90,13 @@ public class CamelVehicleComponent extends VehicleComponent { } @Override - protected Vector3f getInputVelocity(VehicleContext ctx, float speed) { + protected Vector3f getInputVector(VehicleContext ctx, float speed, Vector3f input) { if (isStationary()) { return Vector3f.ZERO; } SessionPlayerEntity player = vehicle.getSession().getPlayerEntity(); - Vector3f inputVelocity = super.getInputVelocity(ctx, speed); + Vector3f inputVelocity = super.getInputVector(ctx, speed, input); float jumpStrength = player.getVehicleJumpStrength(); if (jumpStrength > 0) { @@ -117,11 +117,11 @@ public class CamelVehicleComponent extends VehicleComponent { } @Override - protected Vector2f getVehicleRotation() { + protected Vector2f getRiddenRotation() { if (isStationary()) { return Vector2f.from(vehicle.getYaw(), vehicle.getPitch()); } - return super.getVehicleRotation(); + return super.getRiddenRotation(); } /** diff --git a/core/src/main/java/org/geysermc/geyser/entity/vehicle/ClientVehicle.java b/core/src/main/java/org/geysermc/geyser/entity/vehicle/ClientVehicle.java index e6aaf1daa..c0480ee90 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/vehicle/ClientVehicle.java +++ b/core/src/main/java/org/geysermc/geyser/entity/vehicle/ClientVehicle.java @@ -26,14 +26,18 @@ package org.geysermc.geyser.entity.vehicle; import org.cloudburstmc.math.vector.Vector2f; +import org.cloudburstmc.math.vector.Vector3f; public interface ClientVehicle { VehicleComponent getVehicleComponent(); - Vector2f getAdjustedInput(Vector2f input); + // MojMap LivingEntity#getRiddenInput + Vector3f getRiddenInput(Vector2f input); + // MojMap LivingEntity#getRiddenSpeed float getVehicleSpeed(); + // MojMap Mob#getControllingPassenger boolean isClientControlled(); default boolean canWalkOnLava() { diff --git a/core/src/main/java/org/geysermc/geyser/entity/vehicle/HappyGhastVehicleComponent.java b/core/src/main/java/org/geysermc/geyser/entity/vehicle/HappyGhastVehicleComponent.java new file mode 100644 index 000000000..b332ff5ff --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/entity/vehicle/HappyGhastVehicleComponent.java @@ -0,0 +1,129 @@ +/* + * 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.vehicle; + +import lombok.Getter; +import lombok.Setter; +import org.cloudburstmc.math.vector.Vector3d; +import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; +import org.geysermc.erosion.util.BlockPositionIterator; +import org.geysermc.geyser.entity.type.living.animal.HappyGhastEntity; +import org.geysermc.geyser.entity.type.player.SessionPlayerEntity; +import org.geysermc.geyser.level.block.Blocks; +import org.geysermc.geyser.level.block.Fluid; +import org.geysermc.geyser.level.block.type.BlockState; +import org.geysermc.geyser.level.physics.BoundingBox; +import org.geysermc.geyser.util.MathUtils; +import org.geysermc.mcprotocollib.protocol.data.game.entity.attribute.AttributeType; + +@Setter +@Getter +public class HappyGhastVehicleComponent extends VehicleComponent { + + private float flyingSpeed; + private float cameraDistance; + + public HappyGhastVehicleComponent(HappyGhastEntity vehicle, float stepHeight) { + super(vehicle, stepHeight); + // Happy Ghast has different defaults + flyingSpeed = 0.05f; + moveSpeed = 0.05f; + cameraDistance = 8.0f; + } + + @Override + protected void updateRotation() { + float yaw = vehicle.getYaw() + MathUtils.wrapDegrees(getRiddenRotation().getX() - vehicle.getYaw()) * 0.08f; + vehicle.setYaw(yaw); + vehicle.setHeadYaw(yaw); + } + + @Override + public void onMount() { + super.onMount(); + SessionPlayerEntity playerEntity = vehicle.getSession().getPlayerEntity(); + playerEntity.getDirtyMetadata().put(EntityDataTypes.SEAT_LOCK_RIDER_ROTATION, false); + playerEntity.getDirtyMetadata().put(EntityDataTypes.SEAT_LOCK_RIDER_ROTATION_DEGREES, 181f); + playerEntity.getDirtyMetadata().put(EntityDataTypes.SEAT_THIRD_PERSON_CAMERA_RADIUS, cameraDistance); + playerEntity.getDirtyMetadata().put(EntityDataTypes.SEAT_CAMERA_RELAX_DISTANCE_SMOOTHING, cameraDistance * 0.75f); + playerEntity.getDirtyMetadata().put(EntityDataTypes.CONTROLLING_RIDER_SEAT_INDEX, (byte) 0); + } + + @Override + public void onDismount() { + super.onDismount(); + SessionPlayerEntity playerEntity = vehicle.getSession().getPlayerEntity(); + playerEntity.getDirtyMetadata().put(EntityDataTypes.SEAT_THIRD_PERSON_CAMERA_RADIUS, (float) AttributeType.Builtin.CAMERA_DISTANCE.getDef()); + playerEntity.getDirtyMetadata().put(EntityDataTypes.SEAT_CAMERA_RELAX_DISTANCE_SMOOTHING, cameraDistance * 0.75f); + playerEntity.getDirtyMetadata().put(EntityDataTypes.CONTROLLING_RIDER_SEAT_INDEX, (byte) 0); + } + + /** + * Called every session tick while the player is mounted on the vehicle. + */ + public void tickVehicle() { + if (!vehicle.isClientControlled()) { + return; + } + + VehicleContext ctx = new VehicleContext(); + ctx.loadSurroundingBlocks(); + + // LivingEntity#travelFlying + Fluid fluid = checkForFluid(ctx); + float drag = switch (fluid) { + case WATER -> 0.8f; + case LAVA -> 0.5f; + case EMPTY -> 0.91f; + }; + // HappyGhast#travel + travel(ctx, flyingSpeed * 5.0f / 3.0f); + vehicle.setMotion(vehicle.getMotion().mul(drag)); + } + + private Fluid checkForFluid(VehicleContext ctx) { + Fluid result = Fluid.EMPTY; + + BoundingBox box = boundingBox.clone(); + box.expand(-0.001); + + Vector3d min = box.getMin(); + Vector3d max = box.getMax(); + + BlockPositionIterator iter = BlockPositionIterator.fromMinMax(min.getFloorX(), min.getFloorY(), min.getFloorZ(), max.getFloorX(), max.getFloorY(), max.getFloorZ()); + for (iter.reset(); iter.hasNext(); iter.next()) { + BlockState blockState = ctx.getBlock(iter); + if (blockState.is(Blocks.WATER)) { + return Fluid.WATER; // Water takes priority over lava + } + if (blockState.is(Blocks.LAVA)) { + result = Fluid.LAVA; + } + } + + return result; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/entity/vehicle/VehicleComponent.java b/core/src/main/java/org/geysermc/geyser/entity/vehicle/VehicleComponent.java index 61875ec90..44e922950 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/vehicle/VehicleComponent.java +++ b/core/src/main/java/org/geysermc/geyser/entity/vehicle/VehicleComponent.java @@ -26,6 +26,8 @@ package org.geysermc.geyser.entity.vehicle; import it.unimi.dsi.fastutil.objects.ObjectDoublePair; +import lombok.Getter; +import lombok.Setter; import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.math.TrigMath; import org.cloudburstmc.math.vector.Vector2f; @@ -64,11 +66,15 @@ public class VehicleComponent { private static final float MIN_VELOCITY = 0.003f; protected final T vehicle; + @Getter protected final BoundingBox boundingBox; protected float stepHeight; + @Getter @Setter protected float moveSpeed; protected double gravity; + @Getter @Setter + protected double waterMovementEfficiency; protected int effectLevitation; protected boolean effectSlowFalling; protected boolean effectWeaving; @@ -78,6 +84,7 @@ public class VehicleComponent { this.stepHeight = stepHeight; this.moveSpeed = (float) AttributeType.Builtin.MOVEMENT_SPEED.getDef(); this.gravity = AttributeType.Builtin.GRAVITY.getDef(); + this.waterMovementEfficiency = AttributeType.Builtin.WATER_MOVEMENT_EFFICIENCY.getDef(); double width = vehicle.getBoundingBoxWidth(); double height = vehicle.getBoundingBoxHeight(); @@ -117,10 +124,6 @@ public class VehicleComponent { boundingBox.translate(vec); } - public BoundingBox getBoundingBox() { - return this.boundingBox; - } - public void setEffect(Effect effect, int effectAmplifier) { switch (effect) { case LEVITATION -> effectLevitation = effectAmplifier + 1; @@ -137,14 +140,6 @@ public class VehicleComponent { } } - public void setMoveSpeed(float moveSpeed) { - this.moveSpeed = moveSpeed; - } - - public float getMoveSpeed() { - return moveSpeed; - } - public void setStepHeight(float stepHeight) { this.stepHeight = MathUtils.clamp(stepHeight, 1.0f, 10.0f); } @@ -193,6 +188,16 @@ public class VehicleComponent { } } + /** + * Update the rotation of the vehicle. Should be called once per tick, and before getInputVector. + */ + protected void updateRotation() { + Vector2f rot = getRiddenRotation(); + vehicle.setYaw(rot.getX()); + vehicle.setHeadYaw(rot.getX()); + vehicle.setPitch(rot.getY()); + } + /** * Adds velocity of all colliding fluids to the vehicle, and returns the height of the fluid to use for movement. * @@ -208,6 +213,7 @@ public class VehicleComponent { BlockPositionIterator iter = BlockPositionIterator.fromMinMax(min.getFloorX(), min.getFloorY(), min.getFloorZ(), max.getFloorX(), max.getFloorY(), max.getFloorZ()); + // Mojmap Entity#updateInWaterStateAndDoFluidPushing double waterHeight = getFluidHeightAndApplyMovement(ctx, iter, Fluid.WATER, 0.014, min.getY()); double lavaHeight = getFluidHeightAndApplyMovement(ctx, iter, Fluid.LAVA, vehicle.getSession().getDimensionType().ultrawarm() ? 0.007 : 0.007 / 3, min.getY()); @@ -341,11 +347,12 @@ public class VehicleComponent { } /** - * Java edition returns the zero vector if the length of the input vector is less than 0.0001 + * Java edition returns the zero vector if the length of the input vector is less than 0.00001f */ protected Vector3d javaNormalize(Vector3d vec) { double len = vec.length(); - return len < 1.0E-4 ? Vector3d.ZERO : Vector3d.from(vec.getX() / len, vec.getY() / len, vec.getZ() / len); + // Used to be 1.0E-4 + return len < 1.0E-5F ? Vector3d.ZERO : Vector3d.from(vec.getX() / len, vec.getY() / len, vec.getZ() / len); } protected float getWorldFluidHeight(Fluid fluidType, int blockId) { @@ -373,6 +380,7 @@ public class VehicleComponent { return BlockUtils.getCollision(adjacentBlockId) instanceof SolidCollision; } + // Mojmap: LivingEntity#travelInFluid protected void waterMovement(VehicleContext ctx) { double gravity = getGravity(); float drag = vehicle.getFlag(EntityFlag.SPRINTING) ? 0.9f : 0.8f; // 0.8f: getBaseMovementSpeedMultiplier @@ -380,6 +388,21 @@ public class VehicleComponent { boolean falling = vehicle.getMotion().getY() <= 0; // NOT IMPLEMENTED: depth strider and dolphins grace +// float g = 0.02f; +// float waterMovementEfficiencyMultiplier = (float) waterMovementEfficiency; +// if (!vehicle.isOnGround()) { +// // TODO test +// waterMovementEfficiencyMultiplier *= 0.5f; +// } +// +// if (waterMovementEfficiencyMultiplier > 0.0F) { +// drag += (0.54600006F - drag) * waterMovementEfficiencyMultiplier; +// g += (this.getSpeed() - g) * waterMovementEfficiencyMultiplier; +// } + +// if (this.hasEffect(MobEffects.DOLPHINS_GRACE)) { +// drag = 0.96F; +// } boolean horizontalCollision = travel(ctx, 0.02f); @@ -570,21 +593,25 @@ public class VehicleComponent { * * @return true if there was a horizontal collision */ + // Mojmap: LivingEntity#moveRelative / LivingEntity#move protected boolean travel(VehicleContext ctx, float speed) { Vector3f motion = vehicle.getMotion(); - // Java only does this client side - motion = motion.mul(0.98f); - motion = Vector3f.from( Math.abs(motion.getX()) < MIN_VELOCITY ? 0 : motion.getX(), Math.abs(motion.getY()) < MIN_VELOCITY ? 0 : motion.getY(), Math.abs(motion.getZ()) < MIN_VELOCITY ? 0 : motion.getZ() ); + Vector3f lastRotation = vehicle.getBedrockRotation(); + updateRotation(); + + Vector2f playerInput = vehicle.getSession().getPlayerEntity().getVehicleInput(); + Vector3f riddenInput = vehicle.getRiddenInput(playerInput.mul(0.98f)); + // !isImmobile if (vehicle.isAlive()) { - motion = motion.add(getInputVelocity(ctx, speed)); + motion = motion.add(getInputVector(ctx, speed, riddenInput)); } Vector3f movementMultiplier = getBlockMovementMultiplier(ctx); @@ -639,7 +666,7 @@ public class VehicleComponent { } // Send the new position to the bedrock client and java server - moveVehicle(ctx.centerPos()); + moveVehicle(ctx.centerPos(), lastRotation); vehicle.setMotion(motion); applyBlockCollisionEffects(ctx); @@ -670,41 +697,30 @@ public class VehicleComponent { return false; } - /** - * Translates the player's input into velocity. - * - * @param ctx context - * @param speed multiplier for input - * @return velocity - */ - protected Vector3f getInputVelocity(VehicleContext ctx, float speed) { - Vector2f input = vehicle.getSession().getPlayerEntity().getVehicleInput(); - input = input.mul(0.98f); - input = vehicle.getAdjustedInput(input); - input = normalizeInput(input); + protected Vector3f getInputVector(VehicleContext ctx, float speed, Vector3f input) { + double lenSquared = input.lengthSquared(); + if (lenSquared < 1.0E-7) { + return Vector3f.ZERO; + } + + if (lenSquared > 1.0f) { + input = input.normalize(); + } input = input.mul(speed); - // Match player rotation - float yaw = vehicle.getSession().getPlayerEntity().getYaw(); + // Match vehicle rotation + float yaw = vehicle.getYaw(); float sin = TrigMath.sin(yaw * TrigMath.DEG_TO_RAD); float cos = TrigMath.cos(yaw * TrigMath.DEG_TO_RAD); - return Vector3f.from(input.getX() * cos - input.getY() * sin, 0, input.getY() * cos + input.getX() * sin); - } - - protected Vector2f normalizeInput(Vector2f input) { - float lenSquared = input.lengthSquared(); - if (lenSquared < 1.0E-7) { - return Vector2f.ZERO; - } else if (lenSquared > 1.0) { - return input.normalize(); - } - return input; + return Vector3f.from(input.getX() * cos - input.getZ() * sin, input.getY(), input.getZ() * cos + input.getX() * sin); } /** * Gets the rotation to use for the vehicle. This is based on the player's head rotation. + * + * @return (yaw, pitch) */ - protected Vector2f getVehicleRotation() { + protected Vector2f getRiddenRotation() { LivingEntity player = vehicle.getSession().getPlayerEntity(); return Vector2f.from(player.getYaw(), player.getPitch() * 0.5f); } @@ -714,10 +730,10 @@ public class VehicleComponent { *

* This also updates the session's last vehicle move timestamp. * @param javaPos the new java position of the vehicle + * @param lastRotation the previous rotation of the vehicle (pitch, yaw, headYaw) */ - protected void moveVehicle(Vector3d javaPos) { + protected void moveVehicle(Vector3d javaPos, Vector3f lastRotation) { Vector3f bedrockPos = javaPos.toFloat(); - Vector2f rotation = getVehicleRotation(); MoveEntityDeltaPacket moveEntityDeltaPacket = new MoveEntityDeltaPacket(); moveEntityDeltaPacket.setRuntimeEntityId(vehicle.getGeyserId()); @@ -740,27 +756,24 @@ public class VehicleComponent { } vehicle.setPosition(bedrockPos); - if (vehicle.getYaw() != rotation.getX()) { - moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_YAW); - moveEntityDeltaPacket.setYaw(rotation.getX()); - vehicle.setYaw(rotation.getX()); - } - if (vehicle.getPitch() != rotation.getY()) { + if (vehicle.getPitch() != lastRotation.getX()) { moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_PITCH); - moveEntityDeltaPacket.setPitch(rotation.getY()); - vehicle.setPitch(rotation.getY()); + moveEntityDeltaPacket.setPitch(vehicle.getPitch()); } - if (vehicle.getHeadYaw() != rotation.getX()) { // Same as yaw + if (vehicle.getYaw() != lastRotation.getY()) { + moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_YAW); + moveEntityDeltaPacket.setYaw(vehicle.getYaw()); + } + if (vehicle.getHeadYaw() != lastRotation.getZ()) { moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_HEAD_YAW); - moveEntityDeltaPacket.setHeadYaw(rotation.getX()); - vehicle.setHeadYaw(rotation.getX()); + moveEntityDeltaPacket.setHeadYaw(vehicle.getHeadYaw()); } if (!moveEntityDeltaPacket.getFlags().isEmpty()) { vehicle.getSession().sendUpstreamPacket(moveEntityDeltaPacket); } - ServerboundMoveVehiclePacket moveVehiclePacket = new ServerboundMoveVehiclePacket(javaPos, rotation.getX(), rotation.getY(), vehicle.isOnGround()); + ServerboundMoveVehiclePacket moveVehiclePacket = new ServerboundMoveVehiclePacket(javaPos, vehicle.getYaw(), vehicle.getPitch(), vehicle.isOnGround()); vehicle.getSession().sendDownstreamPacket(moveVehiclePacket); } 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 5fd64c4d4..1c693a52f 100644 --- a/core/src/main/java/org/geysermc/geyser/item/Items.java +++ b/core/src/main/java/org/geysermc/geyser/item/Items.java @@ -685,6 +685,7 @@ public final class Items { public static final Item BLACK_CONCRETE_POWDER = register(new BlockItem(builder(), Blocks.BLACK_CONCRETE_POWDER)); public static final Item TURTLE_EGG = register(new BlockItem(builder(), Blocks.TURTLE_EGG)); public static final Item SNIFFER_EGG = register(new BlockItem(builder(), Blocks.SNIFFER_EGG)); + public static final Item DRIED_GHAST = register(new BlockItem(builder(), Blocks.DRIED_GHAST)); public static final Item DEAD_TUBE_CORAL_BLOCK = register(new BlockItem(builder(), Blocks.DEAD_TUBE_CORAL_BLOCK)); public static final Item DEAD_BRAIN_CORAL_BLOCK = register(new BlockItem(builder(), Blocks.DEAD_BRAIN_CORAL_BLOCK)); public static final Item DEAD_BUBBLE_CORAL_BLOCK = register(new BlockItem(builder(), Blocks.DEAD_BUBBLE_CORAL_BLOCK)); @@ -867,6 +868,22 @@ public final class Items { public static final Item RAIL = register(new BlockItem(builder(), Blocks.RAIL)); public static final Item ACTIVATOR_RAIL = register(new BlockItem(builder(), Blocks.ACTIVATOR_RAIL)); public static final Item SADDLE = register(new Item("saddle", builder())); + public static final Item WHITE_HARNESS = register(new Item("white_harness", builder())); + public static final Item ORANGE_HARNESS = register(new Item("orange_harness", builder())); + public static final Item MAGENTA_HARNESS = register(new Item("magenta_harness", builder())); + public static final Item LIGHT_BLUE_HARNESS = register(new Item("light_blue_harness", builder())); + public static final Item YELLOW_HARNESS = register(new Item("yellow_harness", builder())); + public static final Item LIME_HARNESS = register(new Item("lime_harness", builder())); + public static final Item PINK_HARNESS = register(new Item("pink_harness", builder())); + public static final Item GRAY_HARNESS = register(new Item("gray_harness", builder())); + public static final Item LIGHT_GRAY_HARNESS = register(new Item("light_gray_harness", builder())); + public static final Item CYAN_HARNESS = register(new Item("cyan_harness", builder())); + public static final Item PURPLE_HARNESS = register(new Item("purple_harness", builder())); + public static final Item BLUE_HARNESS = register(new Item("blue_harness", builder())); + public static final Item BROWN_HARNESS = register(new Item("brown_harness", builder())); + public static final Item GREEN_HARNESS = register(new Item("green_harness", builder())); + public static final Item RED_HARNESS = register(new Item("red_harness", builder())); + public static final Item BLACK_HARNESS = register(new Item("black_harness", builder())); public static final Item MINECART = register(new Item("minecart", builder())); public static final Item CHEST_MINECART = register(new Item("chest_minecart", builder())); public static final Item FURNACE_MINECART = register(new Item("furnace_minecart", builder())); @@ -1131,7 +1148,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.WATER_CAULDRON, Blocks.LAVA_CAULDRON)); + public static final Item CAULDRON = register(new BlockItem(builder(), Blocks.CAULDRON, Blocks.LAVA_CAULDRON, Blocks.POWDER_SNOW_CAULDRON, Blocks.WATER_CAULDRON)); public static final Item 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())); @@ -1160,6 +1177,7 @@ public final class Items { public static final Item FOX_SPAWN_EGG = register(new SpawnEggItem("fox_spawn_egg", builder())); public static final Item FROG_SPAWN_EGG = register(new SpawnEggItem("frog_spawn_egg", builder())); public static final Item GHAST_SPAWN_EGG = register(new SpawnEggItem("ghast_spawn_egg", builder())); + public static final Item HAPPY_GHAST_SPAWN_EGG = register(new SpawnEggItem("happy_ghast_spawn_egg", builder())); public static final Item GLOW_SQUID_SPAWN_EGG = register(new SpawnEggItem("glow_squid_spawn_egg", builder())); public static final Item GOAT_SPAWN_EGG = register(new SpawnEggItem("goat_spawn_egg", builder())); public static final Item GUARDIAN_SPAWN_EGG = register(new SpawnEggItem("guardian_spawn_egg", builder())); @@ -1316,6 +1334,7 @@ public final class Items { public static final Item MUSIC_DISC_5 = register(new Item("music_disc_5", builder())); public static final Item MUSIC_DISC_PIGSTEP = register(new Item("music_disc_pigstep", builder())); public static final Item MUSIC_DISC_PRECIPICE = register(new Item("music_disc_precipice", builder())); + public static final Item MUSIC_DISC_TEARS = register(new Item("music_disc_tears", builder())); public static final Item DISC_FRAGMENT_5 = register(new Item("disc_fragment_5", builder())); public static final Item TRIDENT = register(new Item("trident", builder().attackDamage(9.0))); public static final Item NAUTILUS_SHELL = register(new Item("nautilus_shell", builder())); diff --git a/core/src/main/java/org/geysermc/geyser/item/enchantment/Enchantment.java b/core/src/main/java/org/geysermc/geyser/item/enchantment/Enchantment.java index e0b4f6e0f..c69c5b2f9 100644 --- a/core/src/main/java/org/geysermc/geyser/item/enchantment/Enchantment.java +++ b/core/src/main/java/org/geysermc/geyser/item/enchantment/Enchantment.java @@ -28,9 +28,7 @@ package org.geysermc.geyser.item.enchantment; import org.checkerframework.checker.nullness.qual.Nullable; import org.cloudburstmc.nbt.NbtMap; import org.geysermc.geyser.inventory.item.BedrockEnchantment; -import org.geysermc.geyser.item.Items; import org.geysermc.geyser.item.type.Item; -import org.geysermc.geyser.registry.Registries; import org.geysermc.geyser.session.cache.registry.JavaRegistries; import org.geysermc.geyser.session.cache.registry.RegistryEntryContext; import org.geysermc.geyser.session.cache.tags.GeyserHolderSet; @@ -57,12 +55,12 @@ public record Enchantment(String identifier, NbtMap data = context.data(); Set effects = readEnchantmentComponents(data.getCompound("effects")); - GeyserHolderSet supportedItems = GeyserHolderSet.readHolderSet(context.session(), JavaRegistries.ITEM, data.get("supported_items"), itemId -> Registries.JAVA_ITEM_IDENTIFIERS.getOrDefault(itemId.asString(), Items.AIR).javaId()); + GeyserHolderSet supportedItems = GeyserHolderSet.readHolderSet(context.session(), JavaRegistries.ITEM, data.get("supported_items")); int maxLevel = data.getInt("max_level"); int anvilCost = data.getInt("anvil_cost"); - GeyserHolderSet exclusiveSet = GeyserHolderSet.readHolderSet(context.session(), JavaRegistries.ENCHANTMENT, data.get("exclusive_set"), context::getNetworkId); + GeyserHolderSet exclusiveSet = GeyserHolderSet.readHolderSet(JavaRegistries.ENCHANTMENT, data.get("exclusive_set"), context::getNetworkId); BedrockEnchantment bedrockEnchantment = BedrockEnchantment.getByJavaIdentifier(context.id().asString()); 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 index f8abb46ab..d8a20140d 100644 --- a/core/src/main/java/org/geysermc/geyser/item/hashing/ComponentHasher.java +++ b/core/src/main/java/org/geysermc/geyser/item/hashing/ComponentHasher.java @@ -35,6 +35,7 @@ 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.ShadowColor; import net.kyori.adventure.text.format.Style; import net.kyori.adventure.text.format.TextColor; import net.kyori.adventure.text.format.TextDecoration; @@ -96,9 +97,9 @@ public interface ComponentHasher { .optionalNullable("name", COMPONENT, event -> ((HoverEvent.ShowEntity) event.value()).name()); }); - // TODO shadow colours - needs kyori bump MapBuilder