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

Support for 1.21.6

This commit is contained in:
chris
2025-06-26 18:29:23 +02:00
committed by GitHub
149 changed files with 7138 additions and 37844 deletions

View File

@@ -15,7 +15,7 @@ The ultimate goal of this project is to allow Minecraft: Bedrock Edition users t
Special thanks to the DragonProxy project for being a trailblazer in protocol translation and for all the team members who have joined us here!
## Supported Versions
Geyser is currently supporting Minecraft Bedrock 1.21.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.

View File

@@ -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:
*
* <ul>
* <li>If there are multiple dialogs in the additions tag, the {@code minecraft:custom_options} dialog is opened to select a dialog.</li>
* <li>If there is one dialog in the additions tag, that dialog is opened.</li>
* <li>If there are no dialogs in the tag, but there are server links sent to the client, the {@code minecraft:server_links} dialog is opened.</li>
* <li>If all of the above fails, no dialog is opened.</li>
* </ul>
*
* <p>Use {@link GeyserConnection#hasFormOpen()} to check if a dialog was opened.</p>
* @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:
*
* <ul>
* <li>If there are multiple dialogs in the actions tag, the {@code minecraft:quick_actions} dialog is opened to select a dialog.</li>
* <li>If there is one dialog in the actions tag, that dialog is opened.</li>
* <li>If there are no dialogs in the tag, no dialog is opened.</li>
* </ul>
*
* <p>Use {@link GeyserConnection#hasFormOpen()} to check if a dialog was opened.</p>
* @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.

View File

@@ -25,6 +25,6 @@
"depends": {
"fabricloader": ">=0.16.7",
"fabric-api": "*",
"minecraft": ">=1.21.5"
"minecraft": ">=1.21.6"
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<GeyserCommandSource> context) {
GeyserSession session = Objects.requireNonNull(context.sender().connection());
session.openPauseScreenAdditions();
if (!session.hasFormOpen()) {
context.sender().sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.options.fail", session.locale()));
}
}
}

View File

@@ -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<GeyserCommandSource> context) {
GeyserSession session = Objects.requireNonNull(context.sender().connection());
session.openQuickActions();
if (!session.hasFormOpen()) {
context.sender().sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.quickactions.fail", session.locale()));
}
}
}

View File

@@ -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<GlowSquidEntity> GLOW_SQUID;
public static final EntityDefinition<GoatEntity> GOAT;
public static final EntityDefinition<GuardianEntity> GUARDIAN;
public static final EntityDefinition<HappyGhastEntity> HAPPY_GHAST;
public static final EntityDefinition<HoglinEntity> HOGLIN;
public static final EntityDefinition<MinecartEntity> HOPPER_MINECART;
public static final EntityDefinition<HorseEntity> HORSE;
@@ -395,10 +398,6 @@ public final class EntityDefinitions {
.type(EntityType.LLAMA_SPIT)
.heightAndWidth(0.25f)
.build();
PAINTING = EntityDefinition.<PaintingEntity>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<HangingEntity> hangingEntityBase = EntityDefinition.<HangingEntity>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.<ItemFrameEntity>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)

View File

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

View File

@@ -87,11 +87,9 @@ 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);
}
}
@Override
public void moveAbsolute(Vector3f position, float yaw, float pitch, float headYaw, boolean isOnGround, boolean teleported) {

View File

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

View File

@@ -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<Leashable> 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.
*/

View File

@@ -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, ?> direction) {
setDirection(direction.getValue());
}
public abstract void setDirection(Direction direction);
}

View File

@@ -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<ItemStack, ?> 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.

View File

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

View File

@@ -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<EquipmentSlot, GeyserItemStack> 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);
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,9 +528,40 @@ 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.

View File

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

View File

@@ -70,7 +70,7 @@ public class ThrowableEggEntity extends ThrowableItemEntity {
private static String getVariantOrFallback(GeyserSession session, GeyserItemStack stack) {
Holder<Key> 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);

View File

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

View File

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

View File

@@ -123,7 +123,7 @@ public class SquidEntity extends AgeableWaterEntity implements Tickable {
@Override
public boolean canBeLeashed() {
return isNotLeashed();
return true;
}
private void checkInWater() {

View File

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

View File

@@ -66,7 +66,7 @@ public class HoglinEntity extends AnimalEntity {
@Override
public boolean canBeLeashed() {
return isNotLeashed();
return true;
}
@Override

View File

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

View File

@@ -55,7 +55,7 @@ public interface VariantHolder<BedrockVariant extends VariantHolder.BuiltIn> {
* Sets the variant of the entity.
*/
default void setVariantFromJavaId(int variant) {
setBedrockVariant(variantRegistry().fromNetworkId(getSession(), variant));
setBedrockVariant(variantRegistry().value(getSession(), variant));
}
GeyserSession getSession();

View File

@@ -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<BuiltInVariant> variantRegistry() {
return JavaRegistries.PIG_VARIANT;
}
@Override
protected boolean canUseSlot(EquipmentSlot slot) {
return slot != EquipmentSlot.SADDLE ? super.canUseSlot(slot) : this.isAlive() && !this.isBaby();
}
}

View File

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

View File

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

View File

@@ -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);
}
@Override
protected boolean canUseSlot(EquipmentSlot slot) {
return true;
}
}

View File

@@ -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<Item> getFoodTag() {
return ItemTag.LLAMA_FOOD;
}
@Override
protected boolean canUseSlot(EquipmentSlot slot) {
return true;
}
}

View File

@@ -85,6 +85,6 @@ public abstract class TameableEntity extends AnimalEntity {
@Override
public boolean canBeLeashed() {
return isNotLeashed();
return true;
}
}

View File

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

View File

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

View File

@@ -59,7 +59,7 @@ public class ZoglinEntity extends MonsterEntity {
@Override
public boolean canBeLeashed() {
return isNotLeashed();
return true;
}
@Override

View File

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

View File

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

View File

@@ -90,13 +90,13 @@ public class CamelVehicleComponent extends VehicleComponent<CamelEntity> {
}
@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<CamelEntity> {
}
@Override
protected Vector2f getVehicleRotation() {
protected Vector2f getRiddenRotation() {
if (isStationary()) {
return Vector2f.from(vehicle.getYaw(), vehicle.getPitch());
}
return super.getVehicleRotation();
return super.getRiddenRotation();
}
/**

View File

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

View File

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

View File

@@ -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<T extends LivingEntity & ClientVehicle> {
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<T extends LivingEntity & ClientVehicle> {
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<T extends LivingEntity & ClientVehicle> {
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<T extends LivingEntity & ClientVehicle> {
}
}
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<T extends LivingEntity & ClientVehicle> {
}
}
/**
* 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<T extends LivingEntity & ClientVehicle> {
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<T extends LivingEntity & ClientVehicle> {
}
/**
* 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<T extends LivingEntity & ClientVehicle> {
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<T extends LivingEntity & ClientVehicle> {
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<T extends LivingEntity & ClientVehicle> {
*
* @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<T extends LivingEntity & ClientVehicle> {
}
// 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<T extends LivingEntity & ClientVehicle> {
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<T extends LivingEntity & ClientVehicle> {
* <p>
* 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<T extends LivingEntity & ClientVehicle> {
}
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);
}

View File

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

View File

@@ -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<EnchantmentComponent> effects = readEnchantmentComponents(data.getCompound("effects"));
GeyserHolderSet<Item> supportedItems = GeyserHolderSet.readHolderSet(context.session(), JavaRegistries.ITEM, data.get("supported_items"), itemId -> Registries.JAVA_ITEM_IDENTIFIERS.getOrDefault(itemId.asString(), Items.AIR).javaId());
GeyserHolderSet<Item> supportedItems = GeyserHolderSet.readHolderSet(context.session(), JavaRegistries.ITEM, data.get("supported_items"));
int maxLevel = data.getInt("max_level");
int anvilCost = data.getInt("anvil_cost");
GeyserHolderSet<Enchantment> exclusiveSet = GeyserHolderSet.readHolderSet(context.session(), JavaRegistries.ENCHANTMENT, data.get("exclusive_set"), context::getNetworkId);
GeyserHolderSet<Enchantment> exclusiveSet = GeyserHolderSet.readHolderSet(JavaRegistries.ENCHANTMENT, data.get("exclusive_set"), context::getNetworkId);
BedrockEnchantment bedrockEnchantment = BedrockEnchantment.getByJavaIdentifier(context.id().asString());

View File

@@ -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<Style> STYLE = builder -> builder
.optionalNullable("color", COLOR, Style::color)
.optionalNullable("shadow_color", MinecraftHasher.INT.cast(ShadowColor::value), Style::shadowColor)
.optional("bold", DECORATION_STATE, style -> style.decoration(TextDecoration.BOLD), TextDecoration.State.NOT_SET)
.optional("italic", DECORATION_STATE, style -> style.decoration(TextDecoration.ITALIC), TextDecoration.State.NOT_SET)
.optional("underlined", DECORATION_STATE, style -> style.decoration(TextDecoration.UNDERLINED), TextDecoration.State.NOT_SET)

View File

@@ -43,8 +43,11 @@ import org.geysermc.geyser.level.block.Blocks;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.registry.JavaRegistries;
import org.geysermc.geyser.util.MinecraftKey;
import org.geysermc.mcprotocollib.protocol.data.game.Holder;
import org.geysermc.mcprotocollib.protocol.data.game.entity.Effect;
import org.geysermc.mcprotocollib.protocol.data.game.entity.EquipmentSlot;
import org.geysermc.mcprotocollib.protocol.data.game.entity.attribute.AttributeType;
import org.geysermc.mcprotocollib.protocol.data.game.entity.attribute.ModifierOperation;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.GlobalPos;
import org.geysermc.mcprotocollib.protocol.data.game.entity.type.EntityType;
import org.geysermc.mcprotocollib.protocol.data.game.item.HashedStack;
@@ -77,6 +80,7 @@ import org.geysermc.mcprotocollib.protocol.data.game.item.component.Weapon;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.WritableBookContent;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.WrittenBookContent;
import org.geysermc.mcprotocollib.protocol.data.game.level.sound.BuiltinSound;
import org.geysermc.mcprotocollib.protocol.data.game.level.sound.CustomSound;
import java.util.HashMap;
import java.util.HashSet;
@@ -160,7 +164,9 @@ public class DataComponentHashers {
.optional("dispensable", MinecraftHasher.BOOL, Equippable::dispensable, true)
.optional("swappable", MinecraftHasher.BOOL, Equippable::swappable, true)
.optional("damage_on_hurt", MinecraftHasher.BOOL, Equippable::damageOnHurt, true)
.optional("equip_on_interact", MinecraftHasher.BOOL, Equippable::equipOnInteract, false));
.optional("equip_on_interact", MinecraftHasher.BOOL, Equippable::equipOnInteract, false)
.optional("can_be_sheared", MinecraftHasher.BOOL, Equippable::canBeSheared, false)
.optional("shearing_sound", RegistryHasher.SOUND_EVENT, Equippable::shearingSound, BuiltinSound.ITEM_SHEARS_SNIP));
registerMap(DataComponentTypes.REPAIRABLE, builder -> builder
.accept("items", RegistryHasher.ITEM.holderSet(), Function.identity()));
@@ -257,10 +263,10 @@ public class DataComponentHashers {
register(DataComponentTypes.PIG_VARIANT, RegistryHasher.PIG_VARIANT);
register(DataComponentTypes.COW_VARIANT, RegistryHasher.COW_VARIANT);
register(DataComponentTypes.CHICKEN_VARIANT, MinecraftHasher.KEY
.sessionCast((session, holder) -> holder.getOrCompute(id -> JavaRegistries.CHICKEN_VARIANT.keyFromNetworkId(session, id)))); // Why, Mojang?
.sessionCast((session, holder) -> holder.getOrCompute(id -> JavaRegistries.CHICKEN_VARIANT.key(session, id)))); // Why, Mojang?
register(DataComponentTypes.FROG_VARIANT, RegistryHasher.FROG_VARIANT);
register(DataComponentTypes.HORSE_VARIANT, RegistryHasher.HORSE_VARIANT);
register(DataComponentTypes.PAINTING_VARIANT, RegistryHasher.PAINTING_VARIANT.holder());
register(DataComponentTypes.PAINTING_VARIANT, RegistryHasher.PAINTING_VARIANT.cast(Holder::id)); // This can and will throw when a direct holder was received, which is still possible due to a bug in 1.21.6.
register(DataComponentTypes.LLAMA_VARIANT, RegistryHasher.LLAMA_VARIANT);
register(DataComponentTypes.AXOLOTL_VARIANT, RegistryHasher.AXOLOTL_VARIANT);
register(DataComponentTypes.CAT_VARIANT, RegistryHasher.CAT_VARIANT);
@@ -369,6 +375,56 @@ public class DataComponentHashers {
0, 1
)), 0); // TODO identifier lookup
testHash(session, DataComponentTypes.ATTRIBUTE_MODIFIERS, new ItemAttributeModifiers(
List.of(
ItemAttributeModifiers.Entry.builder()
.attribute(AttributeType.Builtin.ATTACK_DAMAGE.getId())
.modifier(ItemAttributeModifiers.AttributeModifier.builder()
.id(MinecraftKey.key("test_modifier_1"))
.amount(2.0)
.operation(ModifierOperation.ADD)
.build())
.slot(ItemAttributeModifiers.EquipmentSlotGroup.ANY)
.display(new ItemAttributeModifiers.Display(ItemAttributeModifiers.DisplayType.DEFAULT, null))
.build(),
ItemAttributeModifiers.Entry.builder()
.attribute(AttributeType.Builtin.JUMP_STRENGTH.getId())
.modifier(ItemAttributeModifiers.AttributeModifier.builder()
.id(MinecraftKey.key("test_modifier_2"))
.amount(4.2)
.operation(ModifierOperation.ADD_MULTIPLIED_TOTAL)
.build())
.slot(ItemAttributeModifiers.EquipmentSlotGroup.HEAD)
.display(new ItemAttributeModifiers.Display(ItemAttributeModifiers.DisplayType.HIDDEN, null))
.build(),
ItemAttributeModifiers.Entry.builder()
.attribute(AttributeType.Builtin.WAYPOINT_RECEIVE_RANGE.getId())
.modifier(ItemAttributeModifiers.AttributeModifier.builder()
.id(MinecraftKey.key("geyser_mc:test_modifier_3"))
.amount(5.4)
.operation(ModifierOperation.ADD_MULTIPLIED_BASE)
.build())
.slot(ItemAttributeModifiers.EquipmentSlotGroup.FEET)
.display(new ItemAttributeModifiers.Display(ItemAttributeModifiers.DisplayType.DEFAULT, null))
.build()
)
), 1889444548);
testHash(session, DataComponentTypes.ATTRIBUTE_MODIFIERS, new ItemAttributeModifiers(
List.of(
ItemAttributeModifiers.Entry.builder()
.attribute(AttributeType.Builtin.WAYPOINT_TRANSMIT_RANGE.getId())
.modifier(ItemAttributeModifiers.AttributeModifier.builder()
.id(MinecraftKey.key("geyser_mc:test_modifier_4"))
.amount(2.0)
.operation(ModifierOperation.ADD)
.build())
.slot(ItemAttributeModifiers.EquipmentSlotGroup.ANY)
.display(new ItemAttributeModifiers.Display(ItemAttributeModifiers.DisplayType.OVERRIDE, Component.text("give me a test")))
.build()
)
), 1375953017);
testHash(session, DataComponentTypes.CUSTOM_MODEL_DATA,
new CustomModelData(List.of(5.0F, 3.0F, -1.0F), List.of(false, true, false), List.of("1", "3", "2"), List.of(3424, -123, 345)), 1947635619);
@@ -414,16 +470,24 @@ public class DataComponentHashers {
testHash(session, DataComponentTypes.ENCHANTABLE, 3, -1834983819);
testHash(session, DataComponentTypes.EQUIPPABLE, new Equippable(EquipmentSlot.BODY, BuiltinSound.ITEM_ARMOR_EQUIP_GENERIC, null, null, null,
true, true, true, false), 1294431019);
true, true, true, false,
false, BuiltinSound.ITEM_SHEARS_SNIP), 1294431019);
testHash(session, DataComponentTypes.EQUIPPABLE, new Equippable(EquipmentSlot.BODY, BuiltinSound.ITEM_ARMOR_EQUIP_CHAIN, MinecraftKey.key("testing"), null, null,
true, true, true, false), 1226203061);
true, true, true, false,
true, BuiltinSound.ITEM_BONE_MEAL_USE), -801616214);
testHash(session, DataComponentTypes.EQUIPPABLE, new Equippable(EquipmentSlot.BODY, BuiltinSound.AMBIENT_CAVE, null, null, null,
false, true, false, false), 1416408052);
false, true, false, false,
false, new CustomSound("testing_equippable", false, 10.0F)), -1145684769);
testHash(session, DataComponentTypes.EQUIPPABLE, new Equippable(EquipmentSlot.BODY, BuiltinSound.ENTITY_BREEZE_WIND_BURST, null, MinecraftKey.key("testing"),
new HolderSet(new int[]{EntityType.ACACIA_BOAT.ordinal()}), false, true, false, false), 1711275245);
new HolderSet(new int[]{EntityType.ACACIA_BOAT.ordinal()}), false, true, false, false,
true, BuiltinSound.BLOCK_NETHERITE_BLOCK_PLACE), -115079770);
testHash(session, DataComponentTypes.EQUIPPABLE, new Equippable(EquipmentSlot.HELMET, BuiltinSound.ITEM_ARMOR_EQUIP_GENERIC, null, null, null,
true, true, true, false), 497790992); // TODO broken because equipment slot names don't match
true, true, true, false,
false, BuiltinSound.ITEM_SHEARS_SNIP), 497790992);
testHash(session, DataComponentTypes.EQUIPPABLE, new Equippable(EquipmentSlot.HELMET, BuiltinSound.ITEM_ARMOR_EQUIP_GENERIC, null, null,
new HolderSet(MinecraftKey.key("aquatic")),
true, true, true, false,
false, BuiltinSound.ITEM_SHEARS_SNIP), 264760955);
testHash(session, DataComponentTypes.REPAIRABLE, new HolderSet(new int[]{Items.AMETHYST_BLOCK.javaId(), Items.PUMPKIN.javaId()}), -36715567);

View File

@@ -49,7 +49,7 @@ public interface MapBuilder<Type> extends UnaryOperator<MapHasher<Type>> {
*
* @param <Type> the type to encode.
*/
static <Type> MapBuilder<Type> empty() {
static <Type> MapBuilder<Type> unit() {
return builder -> builder;
}
}

View File

@@ -30,8 +30,10 @@ import com.google.common.hash.HashCode;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Predicate;
/**
* {@link MapHasher}s are used to encode a {@link Type} to a map-like structure, which is then hashed using a {@link MinecraftHashEncoder}.
@@ -135,11 +137,7 @@ public class MapHasher<Type> {
* @param <Value> the type of the value.
*/
public <Value> MapHasher<Type> optionalNullable(String key, MinecraftHasher<Value> hasher, Function<Type, Value> extractor) {
Value value = extractor.apply(object);
if (value != null) {
acceptConstant(key, hasher, value);
}
return this;
return optionalPredicate(key, hasher, extractor, Objects::nonNull);
}
/**
@@ -166,8 +164,21 @@ public class MapHasher<Type> {
* @param <Value> the type of the value.
*/
public <Value> MapHasher<Type> optional(String key, MinecraftHasher<Value> hasher, Function<Type, Value> extractor, Value defaultValue) {
return optionalPredicate(key, hasher, extractor, value -> !value.equals(defaultValue));
}
/**
* Extracts a {@link Value} from a {@link Type} using the {@code extractor}, and adds it to the map only if {@code predicate} returns {@code true} for it.
*
* @param key the key to put the {@link Value} in.
* @param hasher the hasher used to hash a {@link Value}.
* @param extractor the function that extracts a {@link Value} from a {@link Type}.
* @param predicate the predicate that checks if a {@link Value} should be added to the map. The {@link Value} won't be added to the map if the predicate returns {@code false} for it.
* @param <Value> the type of the value.
*/
public <Value> MapHasher<Type> optionalPredicate(String key, MinecraftHasher<Value> hasher, Function<Type, Value> extractor, Predicate<Value> predicate) {
Value value = extractor.apply(object);
if (!value.equals(defaultValue)) {
if (predicate.test(value)) {
acceptConstant(key, hasher, value);
}
return this;
@@ -202,9 +213,6 @@ public class MapHasher<Type> {
}
public HashCode build() {
if (unhashed != null) {
System.out.println(unhashed);
}
return encoder.map(map);
}
}

View File

@@ -88,7 +88,7 @@ import java.util.stream.IntStream;
@FunctionalInterface
public interface MinecraftHasher<Type> {
MinecraftHasher<Unit> UNIT = (unit, encoder) -> encoder.emptyMap();
MinecraftHasher<Unit> UNIT = unit();
MinecraftHasher<Byte> BYTE = (b, encoder) -> encoder.number(b);
@@ -237,6 +237,13 @@ public interface MinecraftHasher<Type> {
.optionalNullable("filtered", this, Filterable::getOptional));
}
/**
* Creates a hasher that always encodes into an empty map.
*/
static <Type> MinecraftHasher<Type> unit() {
return (value, encoder) -> encoder.emptyMap();
}
/**
* Lazily-initialises the given hasher using {@link Suppliers#memoize(com.google.common.base.Supplier)}.
*/

View File

@@ -50,7 +50,6 @@ import org.geysermc.mcprotocollib.protocol.data.game.Holder;
import org.geysermc.mcprotocollib.protocol.data.game.entity.Effect;
import org.geysermc.mcprotocollib.protocol.data.game.entity.attribute.AttributeType;
import org.geysermc.mcprotocollib.protocol.data.game.entity.attribute.ModifierOperation;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.PaintingVariant;
import org.geysermc.mcprotocollib.protocol.data.game.entity.type.EntityType;
import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.AdventureModePredicate;
@@ -189,14 +188,7 @@ public interface RegistryHasher<DirectType> extends MinecraftHasher<Integer> {
RegistryHasher<?> FROG_VARIANT = registry(JavaRegistries.FROG_VARIANT);
MinecraftHasher<PaintingVariant> DIRECT_PAINTING_VARIANT = MinecraftHasher.mapBuilder(builder -> builder
.accept("width", INT, PaintingVariant::width)
.accept("height", INT, PaintingVariant::height)
.accept("asset_id", KEY, PaintingVariant::assetId)
.optionalNullable("title", ComponentHasher.COMPONENT, PaintingVariant::title)
.optionalNullable("author", ComponentHasher.COMPONENT, PaintingVariant::author));
RegistryHasher<PaintingVariant> PAINTING_VARIANT = registry(JavaRegistries.PAINTING_VARIANT, DIRECT_PAINTING_VARIANT);
RegistryHasher<?> PAINTING_VARIANT = registry(JavaRegistries.PAINTING_VARIANT);
RegistryHasher<?> CAT_VARIANT = registry(JavaRegistries.CAT_VARIANT);
@@ -287,12 +279,22 @@ public interface RegistryHasher<DirectType> extends MinecraftHasher<Integer> {
MinecraftHasher<AdventureModePredicate> ADVENTURE_MODE_PREDICATE = MinecraftHasher.either(BLOCK_PREDICATE,
predicate -> predicate.getPredicates().size() == 1 ? predicate.getPredicates().get(0) : null, BLOCK_PREDICATE.list(), AdventureModePredicate::getPredicates);
MinecraftHasher<ItemAttributeModifiers.DisplayType> ATTRIBUTE_MODIFIER_DISPLAY_TYPE = MinecraftHasher.fromEnum();
MinecraftHasher<ItemAttributeModifiers.Display> ATTRIBUTE_MODIFIER_DISPLAY = ATTRIBUTE_MODIFIER_DISPLAY_TYPE.dispatch(ItemAttributeModifiers.Display::getType,
displayType -> switch (displayType) {
case DEFAULT, HIDDEN -> MapBuilder.unit();
case OVERRIDE -> builder -> builder
.accept("value", ComponentHasher.COMPONENT, ItemAttributeModifiers.Display::getComponent);
});
MinecraftHasher<ItemAttributeModifiers.Entry> ATTRIBUTE_MODIFIER_ENTRY = MinecraftHasher.mapBuilder(builder -> builder
.accept("type", RegistryHasher.ATTRIBUTE, ItemAttributeModifiers.Entry::getAttribute)
.accept("id", KEY, entry -> entry.getModifier().getId())
.accept("amount", DOUBLE, entry -> entry.getModifier().getAmount())
.accept("operation", ATTRIBUTE_MODIFIER_OPERATION, entry -> entry.getModifier().getOperation())
.optional("slot", EQUIPMENT_SLOT_GROUP, ItemAttributeModifiers.Entry::getSlot, ItemAttributeModifiers.EquipmentSlotGroup.ANY));
.optional("slot", EQUIPMENT_SLOT_GROUP, ItemAttributeModifiers.Entry::getSlot, ItemAttributeModifiers.EquipmentSlotGroup.ANY)
.optionalPredicate("display", ATTRIBUTE_MODIFIER_DISPLAY, ItemAttributeModifiers.Entry::getDisplay, display -> display.getType() != ItemAttributeModifiers.DisplayType.DEFAULT));
MinecraftHasher<Consumable.ItemUseAnimation> ITEM_USE_ANIMATION = MinecraftHasher.fromEnum();
@@ -349,12 +351,12 @@ public interface RegistryHasher<DirectType> extends MinecraftHasher<Integer> {
.accept("min_ticks_in_hive", INT, BeehiveOccupant::getMinTicksInHive));
/**
* Creates a hasher that uses the {@link JavaRegistryKey#keyFromNetworkId(GeyserSession, int)} method to turn a network ID into a {@link Key}, and then encodes this key.
* Creates a hasher that uses the {@link JavaRegistryKey#key(GeyserSession, int)} method to turn a network ID into a {@link Key}, and then encodes this key.
*
* @param registry the registry to create a hasher for.
*/
static RegistryHasher<?> registry(JavaRegistryKey<?> registry) {
MinecraftHasher<Integer> hasher = KEY.sessionCast(registry::keyFromNetworkId);
MinecraftHasher<Integer> hasher = KEY.sessionCast(registry::key);
return hasher::hash;
}

View File

@@ -49,7 +49,7 @@ public enum ConsumeEffectType {
<T extends ConsumeEffect> ConsumeEffectType(Class<T> clazz) {
this.clazz = clazz;
this.builder = MapBuilder.empty();
this.builder = MapBuilder.unit();
}
<T extends ConsumeEffect> ConsumeEffectType(Class<T> clazz, MapBuilder<T> builder) {

View File

@@ -31,6 +31,7 @@ import lombok.Getter;
* This enum stores each gamerule along with the value type and the default.
* It is used to construct the list for the settings menu
*/
// TODO gamerules with feature flags (e.g. minecart speed with minecart experiment)
public enum GameRule {
ANNOUNCEADVANCEMENTS("announceAdvancements", true), // JE only
COMMANDBLOCKOUTPUT("commandBlockOutput", true),
@@ -66,7 +67,8 @@ public enum GameRule {
SHOWDEATHMESSAGES("showDeathMessages", true),
SPAWNRADIUS("spawnRadius", 10),
SPECTATORSGENERATECHUNKS("spectatorsGenerateChunks", true), // JE only
UNIVERSALANGER("universalAnger", false); // JE only
UNIVERSALANGER("universalAnger", false),
LOCATORBAR("locatorBar", true);
public static final GameRule[] VALUES = values();

View File

@@ -126,7 +126,7 @@ public abstract class WorldManager {
* @param value The new value for the gamerule
*/
public void setGameRule(GeyserSession session, String name, Object value) {
session.sendCommand("gamerule " + name + " " + value);
session.sendCommandPacket("gamerule " + name + " " + value);
}
/**
@@ -147,16 +147,6 @@ public abstract class WorldManager {
*/
public abstract int getGameRuleInt(GeyserSession session, GameRule gameRule);
/**
* Change the game mode of the given session
*
* @param session The session of the player to change the game mode of
* @param gameMode The game mode to change the player to
*/
public void setPlayerGameMode(GeyserSession session, GameMode gameMode) {
session.sendCommand("gamemode " + gameMode.name().toLowerCase(Locale.ROOT));
}
/**
* Get the default game mode of the server
*
@@ -172,7 +162,7 @@ public abstract class WorldManager {
* @param gameMode the new default game mode
*/
public void setDefaultGameMode(GeyserSession session, GameMode gameMode) {
session.sendCommand("defaultgamemode " + gameMode.name().toLowerCase(Locale.ROOT));
session.sendCommandPacket("defaultgamemode " + gameMode.name().toLowerCase(Locale.ROOT));
}
/**
@@ -182,7 +172,7 @@ public abstract class WorldManager {
* @param difficulty The difficulty to change to
*/
public void setDifficulty(GeyserSession session, Difficulty difficulty) {
session.sendCommand("difficulty " + difficulty.name().toLowerCase(Locale.ROOT));
session.sendCommandPacket("difficulty " + difficulty.name().toLowerCase(Locale.ROOT));
}
/**

View File

@@ -1804,6 +1804,10 @@ public final class Blocks {
.intState(HATCH)));
public static final Block SNIFFER_EGG = register(new Block("sniffer_egg", builder().destroyTime(0.5f)
.intState(HATCH)));
public static final Block DRIED_GHAST = register(new Block("dried_ghast", builder()
.enumState(HORIZONTAL_FACING, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST)
.intState(DRIED_GHAST_HYDRATION_LEVELS)
.booleanState(WATERLOGGED)));
public static final Block DEAD_TUBE_CORAL_BLOCK = register(new Block("dead_tube_coral_block", builder().requiresCorrectToolForDrops().destroyTime(1.5f)));
public static final Block DEAD_BRAIN_CORAL_BLOCK = register(new Block("dead_brain_coral_block", builder().requiresCorrectToolForDrops().destroyTime(1.5f)));
public static final Block DEAD_BUBBLE_CORAL_BLOCK = register(new Block("dead_bubble_coral_block", builder().requiresCorrectToolForDrops().destroyTime(1.5f)));

View File

@@ -119,6 +119,7 @@ public final class Properties {
public static final IntegerProperty STAGE = IntegerProperty.create("stage", 0, 1);
public static final IntegerProperty STABILITY_DISTANCE = IntegerProperty.create("distance", 0, 7);
public static final IntegerProperty RESPAWN_ANCHOR_CHARGES = IntegerProperty.create("charges", 0, 4);
public static final IntegerProperty DRIED_GHAST_HYDRATION_LEVELS = IntegerProperty.create("hydration", 0, 3);
public static final IntegerProperty ROTATION_16 = IntegerProperty.create("rotation", 0, 15);
public static final BasicEnumProperty BED_PART = BasicEnumProperty.create("part", "head", "foot");
public static final EnumProperty<ChestType> CHEST_TYPE = EnumProperty.create("type", ChestType.VALUES);

View File

@@ -43,6 +43,15 @@ public class BoundingBox implements Cloneable {
private double sizeY;
private double sizeZ;
public BoundingBox(Vector3d position, double sizeX, double sizeY, double sizeZ) {
this.middleX = position.getX();
this.middleY = position.getY();
this.middleZ = position.getZ();
this.sizeX = sizeX;
this.sizeY = sizeY;
this.sizeZ = sizeZ;
}
public void translate(double x, double y, double z) {
middleX += x;
middleY += y;
@@ -87,6 +96,10 @@ public class BoundingBox implements Cloneable {
return checkIntersection(offset.getX(), offset.getY(), offset.getZ(), otherBox);
}
public boolean checkIntersection(BoundingBox otherBox) {
return checkIntersection(0, 0, 0, otherBox);
}
public Vector3d getMin() {
double x = middleX - sizeX / 2;
double y = middleY - sizeY / 2;

View File

@@ -27,8 +27,6 @@ package org.geysermc.geyser.network;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.protocol.bedrock.codec.BedrockCodec;
import org.cloudburstmc.protocol.bedrock.codec.v766.Bedrock_v766;
import org.cloudburstmc.protocol.bedrock.codec.v776.Bedrock_v776;
import org.cloudburstmc.protocol.bedrock.codec.v786.Bedrock_v786;
import org.cloudburstmc.protocol.bedrock.codec.v800.Bedrock_v800;
import org.cloudburstmc.protocol.bedrock.codec.v818.Bedrock_v818;
@@ -66,12 +64,6 @@ public final class GameProtocol {
private static final PacketCodec DEFAULT_JAVA_CODEC = MinecraftCodec.CODEC;
static {
SUPPORTED_BEDROCK_CODECS.add(CodecProcessor.processCodec(Bedrock_v766.CODEC.toBuilder()
.minecraftVersion("1.21.50 - 1.21.51")
.build()));
SUPPORTED_BEDROCK_CODECS.add(CodecProcessor.processCodec(Bedrock_v776.CODEC.toBuilder()
.minecraftVersion("1.21.60 - 1.21.62")
.build()));
SUPPORTED_BEDROCK_CODECS.add(CodecProcessor.processCodec(Bedrock_v786.CODEC.toBuilder()
.minecraftVersion("1.21.70 - 1.21.73")
.build()));
@@ -97,14 +89,6 @@ public final class GameProtocol {
/* Bedrock convenience methods to gatekeep features and easily remove the check on version removal */
public static boolean isPreCreativeInventoryRewrite(int protocolVersion) {
return protocolVersion < 776;
}
public static boolean is1_21_70orHigher(GeyserSession session) {
return session.protocolVersion() >= Bedrock_v786.CODEC.getProtocolVersion();
}
public static boolean isTheOneVersionWithBrokenForms(GeyserSession session) {
return session.protocolVersion() == Bedrock_v786.CODEC.getProtocolVersion();
}
@@ -117,6 +101,10 @@ public final class GameProtocol {
return session.protocolVersion() >= Bedrock_v818.CODEC.getProtocolVersion();
}
public static boolean is1_21_80(GeyserSession session) {
return session.protocolVersion() == Bedrock_v800.CODEC.getProtocolVersion();
}
/**
* Gets the {@link PacketCodec} for Minecraft: Java Edition.
*

View File

@@ -255,6 +255,13 @@ public class UpstreamPacketHandler extends LoggingPacketHandler {
stackPacket.getExperiments().add(new ExperimentData("experimental_graphics", true));
}
if (GameProtocol.is1_21_80(session)) {
// Support happy ghasts in .80
stackPacket.getExperiments().add(new ExperimentData("y_2025_drop_2", true));
// Enables the locator bar for 1.21.80 clients
stackPacket.getExperiments().add(new ExperimentData("locator_bar", true));
}
session.sendUpstreamPacket(stackPacket);
}
default -> session.disconnect("disconnectionScreen.resourcePack");

View File

@@ -56,7 +56,7 @@ public class ListRegistry<M> extends Registry<List<M>> {
*/
@Nullable
public M get(int index) {
if (index >= this.mappings.size()) {
if (index < 0 || index >= this.mappings.size()) {
return null;
}

View File

@@ -42,8 +42,6 @@ import org.cloudburstmc.nbt.NbtMap;
import org.cloudburstmc.nbt.NbtMapBuilder;
import org.cloudburstmc.nbt.NbtType;
import org.cloudburstmc.nbt.NbtUtils;
import org.cloudburstmc.protocol.bedrock.codec.v766.Bedrock_v766;
import org.cloudburstmc.protocol.bedrock.codec.v776.Bedrock_v776;
import org.cloudburstmc.protocol.bedrock.codec.v786.Bedrock_v786;
import org.cloudburstmc.protocol.bedrock.codec.v800.Bedrock_v800;
import org.cloudburstmc.protocol.bedrock.codec.v818.Bedrock_v818;
@@ -59,8 +57,7 @@ import org.geysermc.geyser.level.block.type.Block;
import org.geysermc.geyser.level.block.type.BlockState;
import org.geysermc.geyser.level.block.type.FlowerPotBlock;
import org.geysermc.geyser.registry.BlockRegistries;
import org.geysermc.geyser.registry.populator.conversion.Conversion776_766;
import org.geysermc.geyser.registry.populator.conversion.Conversion786_776;
import org.geysermc.geyser.registry.populator.conversion.Conversion800_786;
import org.geysermc.geyser.registry.type.BlockMappings;
import org.geysermc.geyser.registry.type.GeyserBedrockBlock;
@@ -120,9 +117,7 @@ public final class BlockRegistryPopulator {
private static void registerBedrockBlocks() {
var blockMappers = ImmutableMap.<ObjectIntPair<String>, Remapper>builder()
.put(ObjectIntPair.of("1_21_50", Bedrock_v766.CODEC.getProtocolVersion()), Conversion776_766::remapBlock)
.put(ObjectIntPair.of("1_21_60", Bedrock_v776.CODEC.getProtocolVersion()), Conversion786_776::remapBlock)
.put(ObjectIntPair.of("1_21_70", Bedrock_v786.CODEC.getProtocolVersion()), tag -> tag)
.put(ObjectIntPair.of("1_21_70", Bedrock_v786.CODEC.getProtocolVersion()), Conversion800_786::remapBlock)
.put(ObjectIntPair.of("1_21_80", Bedrock_v800.CODEC.getProtocolVersion()), tag -> tag)
.put(ObjectIntPair.of("1_21_90", Bedrock_v818.CODEC.getProtocolVersion()), tag -> tag)
.build();

View File

@@ -45,8 +45,6 @@ import org.cloudburstmc.nbt.NbtMap;
import org.cloudburstmc.nbt.NbtMapBuilder;
import org.cloudburstmc.nbt.NbtType;
import org.cloudburstmc.nbt.NbtUtils;
import org.cloudburstmc.protocol.bedrock.codec.v766.Bedrock_v766;
import org.cloudburstmc.protocol.bedrock.codec.v776.Bedrock_v776;
import org.cloudburstmc.protocol.bedrock.codec.v786.Bedrock_v786;
import org.cloudburstmc.protocol.bedrock.codec.v800.Bedrock_v800;
import org.cloudburstmc.protocol.bedrock.codec.v818.Bedrock_v818;
@@ -72,7 +70,6 @@ import org.geysermc.geyser.item.Items;
import org.geysermc.geyser.item.type.BlockItem;
import org.geysermc.geyser.item.type.Item;
import org.geysermc.geyser.level.block.property.Properties;
import org.geysermc.geyser.network.GameProtocol;
import org.geysermc.geyser.registry.BlockRegistries;
import org.geysermc.geyser.registry.Registries;
import org.geysermc.geyser.registry.type.BlockMappings;
@@ -115,23 +112,29 @@ public class ItemRegistryPopulator {
public static void populate() {
// 1.21.5
Map<Item, Item> itemFallbacks = new HashMap<>();
itemFallbacks.put(Items.BUSH, Items.SHORT_GRASS);
itemFallbacks.put(Items.CACTUS_FLOWER, Items.BUBBLE_CORAL_FAN);
itemFallbacks.put(Items.FIREFLY_BUSH, Items.SHORT_GRASS);
itemFallbacks.put(Items.LEAF_LITTER, Items.PINK_PETALS);
itemFallbacks.put(Items.SHORT_DRY_GRASS, Items.DEAD_BUSH);
itemFallbacks.put(Items.TALL_DRY_GRASS, Items.TALL_GRASS);
itemFallbacks.put(Items.WILDFLOWERS, Items.PINK_PETALS);
itemFallbacks.put(Items.TEST_BLOCK, Items.STRUCTURE_BLOCK);
itemFallbacks.put(Items.TEST_INSTANCE_BLOCK, Items.JIGSAW);
itemFallbacks.put(Items.BLUE_EGG, Items.EGG);
itemFallbacks.put(Items.BROWN_EGG, Items.EGG);
itemFallbacks.put(Items.BLACK_HARNESS, Items.SADDLE);
itemFallbacks.put(Items.BLUE_HARNESS, Items.SADDLE);
itemFallbacks.put(Items.BROWN_HARNESS, Items.SADDLE);
itemFallbacks.put(Items.RED_HARNESS, Items.SADDLE);
itemFallbacks.put(Items.GREEN_HARNESS, Items.SADDLE);
itemFallbacks.put(Items.YELLOW_HARNESS, Items.SADDLE);
itemFallbacks.put(Items.ORANGE_HARNESS, Items.SADDLE);
itemFallbacks.put(Items.MAGENTA_HARNESS, Items.SADDLE);
itemFallbacks.put(Items.LIGHT_BLUE_HARNESS, Items.SADDLE);
itemFallbacks.put(Items.LIME_HARNESS, Items.SADDLE);
itemFallbacks.put(Items.PINK_HARNESS, Items.SADDLE);
itemFallbacks.put(Items.GRAY_HARNESS, Items.SADDLE);
itemFallbacks.put(Items.CYAN_HARNESS, Items.SADDLE);
itemFallbacks.put(Items.PURPLE_HARNESS, Items.SADDLE);
itemFallbacks.put(Items.LIGHT_GRAY_HARNESS, Items.SADDLE);
itemFallbacks.put(Items.WHITE_HARNESS, Items.SADDLE);
itemFallbacks.put(Items.HAPPY_GHAST_SPAWN_EGG, Items.EGG);
itemFallbacks.put(Items.DRIED_GHAST, Items.PLAYER_HEAD);
itemFallbacks.put(Items.MUSIC_DISC_TEARS, Items.MUSIC_DISC_5);
List<PaletteVersion> paletteVersions = new ArrayList<>(5);
paletteVersions.add(new PaletteVersion("1_21_50", Bedrock_v766.CODEC.getProtocolVersion(), itemFallbacks, (item, mapping) -> mapping));
paletteVersions.add(new PaletteVersion("1_21_60", Bedrock_v776.CODEC.getProtocolVersion(), itemFallbacks, (item, mapping) -> mapping));
paletteVersions.add(new PaletteVersion("1_21_70", Bedrock_v786.CODEC.getProtocolVersion()));
paletteVersions.add(new PaletteVersion("1_21_80", Bedrock_v800.CODEC.getProtocolVersion()));
List<PaletteVersion> paletteVersions = new ArrayList<>(2);
paletteVersions.add(new PaletteVersion("1_21_70", Bedrock_v786.CODEC.getProtocolVersion(), itemFallbacks, (item, mapping) -> mapping));
paletteVersions.add(new PaletteVersion("1_21_80", Bedrock_v800.CODEC.getProtocolVersion(), Map.of(Items.MUSIC_DISC_TEARS, Items.MUSIC_DISC_5), (item, mapping) -> mapping));
paletteVersions.add(new PaletteVersion("1_21_90", Bedrock_v818.CODEC.getProtocolVersion()));
GeyserBootstrap bootstrap = GeyserImpl.getInstance().getBootstrap();
@@ -181,9 +184,6 @@ public class ItemRegistryPopulator {
// Used for custom items
int nextFreeBedrockId = 0;
// TODO yeet
List<ItemDefinition> componentItemData = new ObjectArrayList<>();
Int2ObjectMap<ItemDefinition> registry = new Int2ObjectOpenHashMap<>();
Map<String, ItemDefinition> definitions = new Object2ObjectLinkedOpenHashMap<>();
@@ -248,13 +248,7 @@ public class ItemRegistryPopulator {
}
});
List<CreativeItemGroup> creativeItemGroups;
if (GameProtocol.isPreCreativeInventoryRewrite(palette.protocolVersion)) {
creativeItemGroups = new ArrayList<>();
} else {
creativeItemGroups = CreativeItemRegistryPopulator.readCreativeItemGroups(palette, creativeItems);
}
List<CreativeItemGroup> creativeItemGroups = CreativeItemRegistryPopulator.readCreativeItemGroups(palette, creativeItems);
BlockMappings blockMappings = BlockRegistries.BLOCKS.forVersion(palette.protocolVersion());
Set<Item> javaOnlyItems = new ObjectOpenHashSet<>();
@@ -507,9 +501,6 @@ public class ItemRegistryPopulator {
.build(), creativeNetId.get(), customItem.creativeCategory().getAsInt());
creativeItems.add(creativeItemData);
}
// ComponentItemData - used to register some custom properties
componentItemData.add(customMapping.itemDefinition());
customItemOptions.add(Pair.of(customItem.customItemOptions(), customMapping.itemDefinition()));
registry.put(customMapping.integerId(), customMapping.itemDefinition());
@@ -574,7 +565,6 @@ public class ItemRegistryPopulator {
ItemDefinition definition = new SimpleItemDefinition("geysermc:furnace_minecart", furnaceMinecartId, ItemVersion.DATA_DRIVEN, true, registerFurnaceMinecart(furnaceMinecartId));
definitions.put("geysermc:furnace_minecart", definition);
registry.put(definition.getRuntimeId(), definition);
componentItemData.add(definition);
mappings.set(Items.FURNACE_MINECART.javaId(), ItemMapping.builder()
.javaItem(Items.FURNACE_MINECART)
@@ -605,7 +595,6 @@ public class ItemRegistryPopulator {
int customItemId = nextFreeBedrockId++;
NonVanillaItemRegistration registration = CustomItemRegistryPopulator.registerCustomItem(customItem, customItemId, palette.protocolVersion);
componentItemData.add(registration.mapping().getBedrockDefinition());
ItemMapping mapping = registration.mapping();
Item javaItem = registration.javaItem();
while (javaItem.javaId() >= mappings.size()) {
@@ -668,7 +657,6 @@ public class ItemRegistryPopulator {
.creativeItems(creativeItems)
.creativeItemGroups(creativeItemGroups)
.itemDefinitions(registry)
.componentItemData(componentItemData)
.storedItems(new StoredItemMappings(javaItemToMapping))
.javaOnlyItems(javaOnlyItems)
.buckets(buckets)

View File

@@ -33,8 +33,6 @@ import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenCustomHashMap;
import it.unimi.dsi.fastutil.objects.ObjectIntPair;
import org.cloudburstmc.protocol.bedrock.codec.v766.Bedrock_v766;
import org.cloudburstmc.protocol.bedrock.codec.v776.Bedrock_v776;
import org.cloudburstmc.protocol.bedrock.codec.v786.Bedrock_v786;
import org.cloudburstmc.protocol.bedrock.codec.v800.Bedrock_v800;
import org.cloudburstmc.protocol.bedrock.codec.v818.Bedrock_v818;
@@ -70,8 +68,6 @@ public final class TagRegistryPopulator {
};
List<ObjectIntPair<String>> paletteVersions = List.of(
ObjectIntPair.of("1_21_50", Bedrock_v766.CODEC.getProtocolVersion()),
ObjectIntPair.of("1_21_60", Bedrock_v776.CODEC.getProtocolVersion()),
ObjectIntPair.of("1_21_70", Bedrock_v786.CODEC.getProtocolVersion()),
// Not a typo, they're the same file
ObjectIntPair.of("1_21_70", Bedrock_v800.CODEC.getProtocolVersion()),

View File

@@ -1,65 +0,0 @@
/*
* Copyright (c) 2025 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.registry.populator.conversion;
import org.cloudburstmc.nbt.NbtMap;
import org.cloudburstmc.nbt.NbtMapBuilder;
public class Conversion776_766 {
public static NbtMap remapBlock(NbtMap tag) {
// First: Downgrade from 1.21.70
tag = Conversion786_776.remapBlock(tag);
final String name = tag.getString("name");
if (name.equals("minecraft:creaking_heart")) {
NbtMapBuilder builder = tag.getCompound("states").toBuilder();
String value = (String) builder.remove("creaking_heart_state");
builder.putBoolean("active", value.equals("awake"));
return tag.toBuilder().putCompound("states", builder.build()).build();
}
if (name.endsWith("_door") || name.endsWith("fence_gate")) {
NbtMapBuilder builder = tag.getCompound("states").toBuilder();
String cardinalDirection = (String) builder.remove("minecraft:cardinal_direction");
switch (cardinalDirection) {
case "south" -> builder.putInt("direction", 0);
case "west" -> builder.putInt("direction", 1);
case "east" -> builder.putInt("direction", 3);
case "north" -> builder.putInt("direction", 2);
default -> throw new AssertionError("Invalid direction: " + cardinalDirection);
}
NbtMap states = builder.build();
return tag.toBuilder().putCompound("states", states).build();
}
return tag;
}
}

View File

@@ -27,32 +27,13 @@ package org.geysermc.geyser.registry.populator.conversion;
import org.cloudburstmc.nbt.NbtMap;
import static org.geysermc.geyser.registry.populator.conversion.ConversionHelper.withName;
import static org.geysermc.geyser.registry.populator.conversion.ConversionHelper.withoutStates;
public class Conversion786_776 {
public class Conversion800_786 {
public static NbtMap remapBlock(NbtMap nbtMap) {
final String name = nbtMap.getString("name");
if (name.equals("minecraft:bush")) {
return withName(nbtMap, "fern");
}
if (name.equals("minecraft:firefly_bush")) {
return withName(nbtMap, "deadbush");
}
if (name.equals("minecraft:tall_dry_grass") || name.equals("minecraft:short_dry_grass")) {
return withName(nbtMap, "short_grass");
}
if (name.equals("minecraft:cactus_flower")) {
return withName(nbtMap, "unknown");
}
if (name.equals("minecraft:leaf_litter") || name.equals("minecraft:wildflowers")) {
return withoutStates("unknown");
if (name.equals("minecraft:dried_ghast")) {
return ConversionHelper.withoutStates("unknown");
}
return nbtMap;

View File

@@ -71,8 +71,6 @@ public class ItemMappings implements DefinitionRegistry<ItemDefinition> {
List<ItemDefinition> buckets;
List<ItemDefinition> boats;
List<ItemDefinition> componentItemData; // TODO get rid of?
Int2ObjectMap<String> customIdMappings;
Object2ObjectMap<CustomBlockData, ItemDefinition> customBlockItemDefinitions;

View File

@@ -120,6 +120,7 @@ import org.geysermc.geyser.api.entity.type.player.GeyserPlayerEntity;
import org.geysermc.geyser.api.event.bedrock.SessionDisconnectEvent;
import org.geysermc.geyser.api.event.bedrock.SessionLoginEvent;
import org.geysermc.geyser.api.network.RemoteServer;
import org.geysermc.geyser.command.CommandRegistry;
import org.geysermc.geyser.command.GeyserCommandSource;
import org.geysermc.geyser.configuration.EmoteOffhandWorkaroundOption;
import org.geysermc.geyser.configuration.GeyserConfiguration;
@@ -173,9 +174,14 @@ import org.geysermc.geyser.session.cache.SkullCache;
import org.geysermc.geyser.session.cache.StructureBlockCache;
import org.geysermc.geyser.session.cache.TagCache;
import org.geysermc.geyser.session.cache.TeleportCache;
import org.geysermc.geyser.session.cache.waypoint.WaypointCache;
import org.geysermc.geyser.session.cache.WorldBorder;
import org.geysermc.geyser.session.cache.WorldCache;
import org.geysermc.geyser.session.cache.registry.JavaRegistries;
import org.geysermc.geyser.session.cache.tags.DialogTag;
import org.geysermc.geyser.session.dialog.BuiltInDialog;
import org.geysermc.geyser.session.dialog.Dialog;
import org.geysermc.geyser.session.dialog.DialogManager;
import org.geysermc.geyser.text.GeyserLocale;
import org.geysermc.geyser.translator.inventory.InventoryTranslator;
import org.geysermc.geyser.translator.text.MessageTranslator;
@@ -194,6 +200,7 @@ import org.geysermc.mcprotocollib.protocol.ClientListener;
import org.geysermc.mcprotocollib.protocol.MinecraftConstants;
import org.geysermc.mcprotocollib.protocol.MinecraftProtocol;
import org.geysermc.mcprotocollib.protocol.data.ProtocolState;
import org.geysermc.mcprotocollib.protocol.data.game.ServerLink;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.Pose;
import org.geysermc.mcprotocollib.protocol.data.game.entity.object.Direction;
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.GameMode;
@@ -282,6 +289,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
private final SkullCache skullCache;
private final StructureBlockCache structureBlockCache;
private final TagCache tagCache;
private final WaypointCache waypointCache;
private final WorldCache worldCache;
@Setter
@@ -307,6 +315,25 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
@Setter
private @Nullable InventoryHolder<? extends Inventory> inventoryHolder;
private final DialogManager dialogManager = new DialogManager(this);
/**
* A list of links sent to us by the server in the server links packet.
*/
@Setter
private List<ServerLink> serverLinks = List.of();
/**
* A list of commands known to the client. These are all the commands that have been sent to us by the server.
*/
@Setter
private List<String> knownCommands = List.of();
/**
* A list of "restricted" commands known to the client. These are all the commands that have been sent to us by the server, and require some sort of elevated permissions.
*/
@Setter
private List<String> restrictedCommands = List.of();
/**
* Whether the client is currently closing an inventory.
* Used to open new inventories while another one is currently open.
@@ -395,6 +422,12 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
private boolean sneaking;
/**
* Used to send a shift state for a tick to dismount from entitites
*/
@Setter
private boolean shouldSendSneak;
/**
* Stores the Java pose that the server and/or Geyser believes the player currently has.
*/
@@ -714,6 +747,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
this.skullCache = new SkullCache(this);
this.structureBlockCache = new StructureBlockCache();
this.tagCache = new TagCache(this);
this.waypointCache = new WaypointCache(this);
this.worldCache = new WorldCache(this);
this.cameraData = new GeyserCameraData(this);
this.entityData = new GeyserEntityData(this);
@@ -777,15 +811,9 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
sentSpawnPacket = true;
syncEntityProperties();
if (GameProtocol.isPreCreativeInventoryRewrite(this.protocolVersion())) {
ItemComponentPacket componentPacket = new ItemComponentPacket();
componentPacket.getItems().addAll(itemMappings.getComponentItemData());
upstream.sendPacket(componentPacket);
} else {
ItemComponentPacket componentPacket = new ItemComponentPacket();
componentPacket.getItems().addAll(itemMappings.getItemDefinitions().values());
upstream.sendPacket(componentPacket);
}
ChunkUtils.sendEmptyChunks(this, playerEntity.getPosition().toInt(), 0, false);
@@ -839,7 +867,8 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
gamerulePacket.getGameRules().add(new GameRuleData<>("spawnradius", 0));
// Recipe unlocking
gamerulePacket.getGameRules().add(new GameRuleData<>("recipesunlock", true));
// Disable locator bar for now
// We disable the locator bar until we are certain that the server wants us to enable it
// See WaypointCache for details
gamerulePacket.getGameRules().add(new GameRuleData<>("locatorBar", false));
upstream.sendPacket(gamerulePacket);
@@ -1242,6 +1271,9 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
// but this will work once we implement matching Java custom tick cycles
sendDownstreamGamePacket(ServerboundClientTickEndPacket.INSTANCE);
}
dialogManager.tick();
waypointCache.tick();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
@@ -1250,20 +1282,20 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
worldTicks++;
}
public void startSneaking() {
public void startSneaking(boolean updateMetaData) {
// Toggle the shield, if there is no ongoing arm animation
// This matches Bedrock Edition behavior as of 1.18.12
if (armAnimationTicks < 0) {
attemptToBlock();
}
setSneaking(true);
setSneaking(true, updateMetaData);
}
public void stopSneaking() {
public void stopSneaking(boolean updateMetaData) {
disableBlocking();
setSneaking(false);
setSneaking(false, updateMetaData);
}
public void setSpinAttack(boolean spinAttack) {
@@ -1274,7 +1306,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
switchPose(gliding, EntityFlag.GLIDING, Pose.FALL_FLYING);
}
private void setSneaking(boolean sneaking) {
private void setSneaking(boolean sneaking, boolean update) {
this.sneaking = sneaking;
// Update pose and bounding box on our end
@@ -1284,7 +1316,9 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
}
collisionManager.updateScaffoldingFlags(false);
if (update) {
playerEntity.updateBedrockMetadata();
}
if (mouseoverEntity != null) {
// Horses, etc can change their property depending on if you're sneaking
@@ -1481,10 +1515,65 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
/**
* Sends a command to the Java server.
*/
public void sendCommand(String command) {
public void sendCommandPacket(String command) {
sendDownstreamGamePacket(new ServerboundChatCommandSignedPacket(command, Instant.now().toEpochMilli(), 0L, Collections.emptyList(), 0, new BitSet(), (byte) 0));
}
/**
* Runs the command through platform specific command registries if applicable
* else, it sends the command to the server.
*/
@Override
public void sendCommand(String command) {
if (MessageTranslator.isTooLong(command, this)) {
return;
}
if (CommandRegistry.STANDALONE_COMMAND_MANAGER) {
// try to handle the command within the standalone/viaproxy command manager
String[] args = command.split(" ");
if (args.length > 0) {
String root = args[0];
CommandRegistry registry = GeyserImpl.getInstance().commandRegistry();
if (registry.rootCommands().contains(root)) {
registry.runCommand(this, command);
// don't pass the command to the java server here
// will pass it through later if the user lacks permission
return;
}
}
}
this.sendCommandPacket(command);
}
@Override
public void openPauseScreenAdditions() {
List<Dialog> additions = tagCache.get(DialogTag.PAUSE_SCREEN_ADDITIONS);
if (additions.isEmpty()) {
if (!serverLinks.isEmpty()) {
dialogManager.openDialog(BuiltInDialog.SERVER_LINKS);
}
} else if (additions.size() == 1) {
dialogManager.openDialog(additions.get(0));
} else {
dialogManager.openDialog(BuiltInDialog.CUSTOM_OPTIONS);
}
}
@Override
public void openQuickActions() {
List<Dialog> quickActions = tagCache.get(DialogTag.QUICK_ACTIONS);
if (quickActions.isEmpty()) {
return;
} else if (quickActions.size() == 1) {
dialogManager.openDialog(quickActions.get(0));
} else {
dialogManager.openDialog(BuiltInDialog.QUICK_ACTIONS);
}
}
public void setClientRenderDistance(int clientRenderDistance) {
boolean oldSquareToCircle = this.clientRenderDistance < this.serverRenderDistance;
this.clientRenderDistance = clientRenderDistance;
@@ -1521,6 +1610,19 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
@Override
public boolean sendForm(@NonNull Form form) {
// First close any dialogs that are open. This won't execute the dialog's closing action.
dialogManager.close();
return doSendForm(form);
}
/**
* Sends a form without first closing any open dialog. This should only be used by {@link org.geysermc.geyser.session.dialog.Dialog}s.
*/
public boolean sendDialogForm(@NonNull Form form) {
return doSendForm(form);
}
private boolean doSendForm(@NonNull Form form) {
formCache.showForm(form);
return true;
}
@@ -1629,6 +1731,12 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
if (allowVibrantVisuals && !GameProtocol.is1_21_90orHigher(this)) {
startGamePacket.getExperiments().add(new ExperimentData("experimental_graphics", true));
}
// Enables 2025 Content Drop 2 features
if (GameProtocol.is1_21_80(this)) {
startGamePacket.getExperiments().add(new ExperimentData("y_2025_drop_2", true));
// Enables the locator bar for 1.21.80 clients
startGamePacket.getExperiments().add(new ExperimentData("locator_bar", true));
}
startGamePacket.setVanillaVersion("*");
startGamePacket.setInventoriesServerAuthoritative(true);
@@ -1795,7 +1903,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
/**
* Changes the daylight cycle gamerule on the client
* This is used in the login screen along-side normal usage
* This is used in login and configuration screens along-side normal usage
*
* @param doCycle If the cycle should continue
*/
@@ -2210,6 +2318,11 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
return upstream.getProtocolVersion();
}
@Override
public boolean hasFormOpen() {
return formCache.hasFormOpen();
}
@Override
public void closeForm() {
sendUpstreamPacket(new ClientboundCloseFormPacket());

View File

@@ -64,6 +64,10 @@ public class FormCache {
private final Int2ObjectMap<Form> forms = new Int2ObjectOpenHashMap<>();
private final GeyserSession session;
public boolean hasFormOpen() {
return !forms.isEmpty();
}
public int addForm(Form form) {
int formId = formIdCounter.getAndIncrement();
forms.put(formId, form);
@@ -111,9 +115,7 @@ public class FormCache {
}
String responseData = response.getFormData();
//todo work on a proper solution in Cumulus, but that'd require all Floodgate instances to update as well and
// drops support for older Bedrock versions (because Cumulus isn't made to support multiple versions). That's
// why this hotfix exists.
// TODO drop once 1.21.70 is no longer supported
if (form instanceof CustomForm customForm && GameProtocol.isTheOneVersionWithBrokenForms(session) && response.getCancelReason().isEmpty()) {
// Labels are no longer included as a json null, so we have to manually add them for now.
IntList labelIndexes = new IntArrayList();

View File

@@ -34,9 +34,7 @@ import org.cloudburstmc.protocol.bedrock.data.PlayerAuthInputData;
import org.cloudburstmc.protocol.bedrock.packet.PlayerAuthInputPacket;
import org.geysermc.geyser.entity.type.player.SessionPlayerEntity;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.PlayerState;
import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.level.ServerboundPlayerInputPacket;
import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundPlayerCommandPacket;
import java.util.Set;
@@ -78,6 +76,8 @@ public final class InputCache {
right = analogMovement.getX() < 0;
}
boolean sneaking = isSneaking(bedrockInput);
// TODO when is UP_LEFT, etc. used?
this.inputPacket = this.inputPacket
.withForward(up)
@@ -88,18 +88,16 @@ public final class InputCache {
// using the "raw" values allows us sending key presses even with locked input
// There appear to be cases where the raw value is not sent - e.g. sneaking with a shield on mobile (1.21.80)
.withJump(bedrockInput.contains(PlayerAuthInputData.JUMP_CURRENT_RAW) || bedrockInput.contains(PlayerAuthInputData.JUMP_DOWN))
.withShift(bedrockInput.contains(PlayerAuthInputData.SNEAK_CURRENT_RAW) || bedrockInput.contains(PlayerAuthInputData.SNEAK_DOWN))
.withShift(session.isShouldSendSneak() || sneaking)
.withSprint(bedrockInput.contains(PlayerAuthInputData.SPRINT_DOWN));
// Send sneaking before inputs; matches Java edition
boolean sneaking = isSneaking(bedrockInput);
// TODO - test whether we can rely on the Java server setting sneaking for us.
// 1.21.6+ only sends the shifting state in the input packet, and removed the START/STOP sneak command packet sending
if (session.isSneaking() != sneaking) {
if (sneaking) {
session.sendDownstreamGamePacket(new ServerboundPlayerCommandPacket(entity.javaId(), PlayerState.START_SNEAKING));
session.startSneaking();
session.startSneaking(true);
} else {
session.sendDownstreamGamePacket(new ServerboundPlayerCommandPacket(entity.javaId(), PlayerState.STOP_SNEAKING));
session.stopSneaking();
session.stopSneaking(true);
}
}

View File

@@ -53,6 +53,7 @@ import org.geysermc.geyser.session.cache.registry.RegistryEntryContext;
import org.geysermc.geyser.session.cache.registry.RegistryEntryData;
import org.geysermc.geyser.session.cache.registry.RegistryUnit;
import org.geysermc.geyser.session.cache.registry.SimpleJavaRegistry;
import org.geysermc.geyser.session.dialog.Dialog;
import org.geysermc.geyser.text.ChatDecoration;
import org.geysermc.geyser.translator.level.BiomeTranslator;
import org.geysermc.geyser.util.MinecraftKey;
@@ -87,6 +88,7 @@ public final class RegistryCache {
register(JavaRegistries.TRIM_MATERIAL, TrimRecipe::readTrimMaterial);
register(JavaRegistries.TRIM_PATTERN, TrimRecipe::readTrimPattern);
register(JavaRegistries.DAMAGE_TYPE, RegistryReader.UNIT);
register(JavaRegistries.DIALOG, Dialog::readDialog);
register(JavaRegistries.CAT_VARIANT, VariantHolder.reader(CatEntity.BuiltInVariant.class, CatEntity.BuiltInVariant.BLACK));
register(JavaRegistries.FROG_VARIANT, VariantHolder.reader(FrogEntity.BuiltInVariant.class, FrogEntity.BuiltInVariant.TEMPERATE));
@@ -135,7 +137,11 @@ public final class RegistryCache {
// Java generic mess - we're sure we're putting the current readers for the correct registry types in the READERS map, so we use raw objects here to let it compile
RegistryLoader reader = READERS.get(registryKey);
if (reader != null) {
try {
reader.load(session, registries.get(registryKey), packet.getEntries());
} catch (Exception exception) {
GeyserImpl.getInstance().getLogger().error("Failed parsing registry entries for " + registryKey + "!", exception);
}
} else {
throw new IllegalStateException("Expected reader for registry " + registryKey);
}
@@ -187,7 +193,7 @@ public final class RegistryCache {
// Registry readers should never return null, rather return a default value
throw new IllegalStateException("Registry reader returned null for an entry!");
}
builder.add(i, new RegistryEntryData<>(entry.getId(), cacheEntry));
builder.add(i, new RegistryEntryData<>(i, entry.getId(), cacheEntry));
}
registry.reset(builder);
});

View File

@@ -62,7 +62,7 @@ public final class TagCache {
this.session = session;
}
public void loadPacket(GeyserSession session, ClientboundUpdateTagsPacket packet) {
public void loadPacket(ClientboundUpdateTagsPacket packet) {
Map<Key, Map<Key, int[]>> allTags = packet.getTags();
GeyserLogger logger = session.getGeyser().getLogger();
@@ -110,7 +110,7 @@ public final class TagCache {
}
public <T> boolean is(Tag<T> tag, T object) {
return contains(getRaw(tag), tag.registry().toNetworkId(session, object));
return contains(getRaw(tag), tag.registry().networkId(session, object));
}
/**
@@ -127,7 +127,7 @@ public final class TagCache {
if (holderSet == null || object == null) {
return false;
}
return contains(holderSet.resolveRaw(this), holderSet.getRegistry().toNetworkId(session, object));
return contains(holderSet.resolveRaw(this), holderSet.getRegistry().networkId(session, object));
}
/**
@@ -173,7 +173,7 @@ public final class TagCache {
* Maps a raw array of network IDs to their respective objects.
*/
public static <T> List<T> mapRawArray(GeyserSession session, int[] array, JavaRegistryKey<T> registry) {
return Arrays.stream(array).mapToObj(i -> registry.fromNetworkId(session, i)).toList();
return Arrays.stream(array).mapToObj(i -> registry.value(session, i)).toList();
}
private static boolean contains(int[] array, int i) {

View File

@@ -44,11 +44,18 @@ import org.geysermc.geyser.level.block.type.Block;
import org.geysermc.geyser.registry.BlockRegistries;
import org.geysermc.geyser.registry.ListRegistry;
import org.geysermc.geyser.registry.Registries;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.dialog.Dialog;
import org.geysermc.geyser.util.MinecraftKey;
import org.geysermc.mcprotocollib.protocol.data.game.chat.ChatType;
import org.geysermc.mcprotocollib.protocol.data.game.entity.type.EntityType;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
/**
* Stores {@link JavaRegistryKey} for Java registries that are used for loading of data-driven objects, tags, or both. Read {@link JavaRegistryKey} for more information on how to use one.
@@ -56,8 +63,18 @@ import java.util.List;
public class JavaRegistries {
private static final List<JavaRegistryKey<?>> VALUES = new ArrayList<>();
public static final JavaRegistryKey<Block> BLOCK = createHardcoded("block", BlockRegistries.JAVA_BLOCKS, Block::javaId, Block::javaIdentifier);
public static final JavaRegistryKey<Item> ITEM = createHardcoded("item", Registries.JAVA_ITEMS, Item::javaId, Item::javaKey);
public static final JavaRegistryKey<Block> BLOCK = createHardcoded("block", BlockRegistries.JAVA_BLOCKS,
Block::javaId, Block::javaIdentifier, key -> Optional.ofNullable(BlockRegistries.JAVA_IDENTIFIER_TO_ID.get(key.asString())).orElse(-1));
public static final JavaRegistryKey<Item> ITEM = createHardcoded("item", Registries.JAVA_ITEMS,
Item::javaId, Item::javaKey, key -> Optional.ofNullable(Registries.JAVA_ITEM_IDENTIFIERS.get(key.asString())).map(Item::javaId).orElse(-1));
public static JavaRegistryKey<EntityType> ENTITY_TYPE = createHardcoded("entity_type", Arrays.asList(EntityType.values()), EntityType::ordinal,
type -> MinecraftKey.key(type.name().toLowerCase(Locale.ROOT)), key -> {
try {
return EntityType.valueOf(key.value().toUpperCase(Locale.ROOT)).ordinal();
} catch (IllegalArgumentException exception) {
return -1; // Non-existent entity type
}
});
public static final JavaRegistryKey<ChatType> CHAT_TYPE = create("chat_type");
public static final JavaRegistryKey<JavaDimension> DIMENSION_TYPE = create("dimension_type");
@@ -70,6 +87,7 @@ public class JavaRegistries {
public static final JavaRegistryKey<TrimMaterial> TRIM_MATERIAL = create("trim_material");
public static final JavaRegistryKey<TrimPattern> TRIM_PATTERN = create("trim_pattern");
public static final JavaRegistryKey<RegistryUnit> DAMAGE_TYPE = create("damage_type");
public static final JavaRegistryKey<Dialog> DIALOG = create("dialog");
public static final JavaRegistryKey<CatEntity.BuiltInVariant> CAT_VARIANT = create("cat_variant");
public static final JavaRegistryKey<FrogEntity.BuiltInVariant> FROG_VARIANT = create("frog_variant");
@@ -80,23 +98,24 @@ public class JavaRegistries {
public static final JavaRegistryKey<TemperatureVariantAnimal.BuiltInVariant> COW_VARIANT = create("cow_variant");
public static final JavaRegistryKey<TemperatureVariantAnimal.BuiltInVariant> CHICKEN_VARIANT = create("chicken_variant");
private static <T> JavaRegistryKey<T> create(String key, JavaRegistryKey.NetworkSerializer<T> networkSerializer, JavaRegistryKey.NetworkDeserializer<T> networkDeserializer,
JavaRegistryKey.NetworkIdentifier<T> networkIdentifier) {
JavaRegistryKey<T> registry = new JavaRegistryKey<>(MinecraftKey.key(key), networkSerializer, networkDeserializer, networkIdentifier);
private static <T> JavaRegistryKey<T> create(String key, JavaRegistryKey.RegistryLookup<T> registryLookup) {
JavaRegistryKey<T> registry = new JavaRegistryKey<>(MinecraftKey.key(key), registryLookup);
VALUES.add(registry);
return registry;
}
private static <T> JavaRegistryKey<T> createHardcoded(String key, ListRegistry<T> registry, RegistryNetworkMapper<T> networkSerializer, RegistryIdentifierMapper<T> identifierMapper) {
return create(key, (session, $, object) -> networkSerializer.get(object),
(session, $, id) -> registry.get(id),
(session, $, id) -> identifierMapper.get(registry.get(id)));
private static <T> JavaRegistryKey<T> createHardcoded(String key, ListRegistry<T> registry, RegistryNetworkMapper<T> networkSerializer,
RegistryIdentifierMapper<T> identifierMapper, RegistryIdMapper idMapper) {
return createHardcoded(key, registry.get(), networkSerializer, identifierMapper, idMapper);
}
private static <T> JavaRegistryKey<T> createHardcoded(String key, List<T> registry, RegistryNetworkMapper<T> networkSerializer,
RegistryIdentifierMapper<T> identifierMapper, RegistryIdMapper idMapper) {
return create(key, new HardcodedLookup<>(registry, networkSerializer, identifierMapper, idMapper));
}
private static <T> JavaRegistryKey<T> create(String key) {
return create(key, (session, registry, object) -> session.getRegistryCache().registry(registry).byValue(object),
(session, registry, id) -> session.getRegistryCache().registry(registry).byId(id),
(session, registry, id) -> session.getRegistryCache().registry(registry).entryById(id).key());
return create(key, new RegistryCacheLookup<>());
}
@Nullable
@@ -120,4 +139,55 @@ public class JavaRegistries {
Key get(T object);
}
@FunctionalInterface
interface RegistryIdMapper {
int get(Key key);
}
private record HardcodedLookup<T>(List<T> registry, RegistryNetworkMapper<T> networkMapper, RegistryIdentifierMapper<T> identifierMapper,
RegistryIdMapper idMapper) implements JavaRegistryKey.RegistryLookup<T> {
@Override
public Optional<RegistryEntryData<T>> entry(GeyserSession session, JavaRegistryKey<T> registryKey, int networkId) {
return Optional.ofNullable(registry.get(networkId))
.map(value -> new RegistryEntryData<>(networkId, Objects.requireNonNull(identifierMapper.get(value)), value));
}
@Override
public Optional<RegistryEntryData<T>> entry(GeyserSession session, JavaRegistryKey<T> registryKey, Key key) {
int id = idMapper.get(key);
return Optional.ofNullable(registry.get(id)).map(value -> new RegistryEntryData<>(id, key, value));
}
@Override
public Optional<RegistryEntryData<T>> entry(GeyserSession session, JavaRegistryKey<T> registryKey, T object) {
int id = networkMapper.get(object);
return Optional.ofNullable(registry.get(id))
.map(value -> new RegistryEntryData<>(id, Objects.requireNonNull(identifierMapper.get(value)), value));
}
}
private static class RegistryCacheLookup<T> implements JavaRegistryKey.RegistryLookup<T> {
@Override
public Optional<RegistryEntryData<T>> entry(GeyserSession session, JavaRegistryKey<T> registryKey, int networkId) {
return Optional.ofNullable(registry(session, registryKey).entryById(networkId));
}
@Override
public Optional<RegistryEntryData<T>> entry(GeyserSession session, JavaRegistryKey<T> registryKey, Key key) {
return Optional.ofNullable(registry(session, registryKey).entryByKey(key));
}
@Override
public Optional<RegistryEntryData<T>> entry(GeyserSession session, JavaRegistryKey<T> registryKey, T object) {
return Optional.ofNullable(registry(session, registryKey).entryByValue(object));
}
private JavaRegistry<T> registry(GeyserSession session, JavaRegistryKey<T> key) {
return session.getRegistryCache().registry(key);
}
}
}

View File

@@ -27,6 +27,7 @@ package org.geysermc.geyser.session.cache.registry;
import net.kyori.adventure.key.Key;
import org.checkerframework.checker.index.qual.NonNegative;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.util.List;
@@ -38,17 +39,22 @@ public interface JavaRegistry<T> {
/**
* Looks up a registry entry by its ID. The object can be null, or not present.
*/
T byId(@NonNegative int id);
/**
* Looks up a registry entry by its key. The object can be null, or not present.
*/
T byKey(Key key);
@Nullable T byId(@NonNegative int id);
/**
* Looks up a registry entry by its ID, and returns it wrapped in {@link RegistryEntryData} so that its registered key is also known. The object can be null, or not present.
*/
RegistryEntryData<T> entryById(@NonNegative int id);
@Nullable RegistryEntryData<T> entryById(@NonNegative int id);
/**
* Looks up a registry entry by its key. The object can be null, or not present.
*/
@Nullable T byKey(Key key);
/**
* Looks up a registry entry by its key, and returns it wrapped in {@link RegistryEntryData}. The object can be null, or not present.
*/
@Nullable RegistryEntryData<T> entryByKey(Key key);
/**
* Reverse looks-up an object to return its network ID, or -1.
@@ -58,7 +64,7 @@ public interface JavaRegistry<T> {
/**
* Reverse looks-up an object to return it wrapped in {@link RegistryEntryData}, or null.
*/
RegistryEntryData<T> entryByValue(T value);
@Nullable RegistryEntryData<T> entryByValue(T value);
/**
* Resets the objects by these IDs.

View File

@@ -26,8 +26,12 @@
package org.geysermc.geyser.session.cache.registry;
import net.kyori.adventure.key.Key;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.geyser.session.GeyserSession;
import java.util.Optional;
/**
* Defines a Java registry, which can be hardcoded or data-driven. This class doesn't store registry contents itself, that is handled by {@link org.geysermc.geyser.session.cache.RegistryCache} in the case of
* data-driven registries and other classes in the case of hardcoded registries.
@@ -35,50 +39,82 @@ import org.geysermc.geyser.session.GeyserSession;
* <p>This class is used when, for a Java registry, data-driven objects and/or tags need to be loaded. Only one instance of this class should be created for each Java registry. Instances of this
* class are kept in {@link JavaRegistries}, which also has useful methods for creating instances of this class.</p>
*
* <p>This class has a few handy utility methods to convert between the various representations of an object in a registry (network ID, resource location/key, value).</p>
*
* @param registryKey the registry key, as it appears on Java.
* @param networkSerializer a method that converts an object in this registry to its network ID.
* @param networkDeserializer a method that converts a network ID to an object in this registry.
* @param networkIdentifier a method that converts a network ID to its respective key in this registry.
* @param lookup an implementation of {@link RegistryLookup} that converts an object in this registry to its respective network ID or key, and back.
* @param <T> the object type this registry holds.
*/
public record JavaRegistryKey<T>(Key registryKey, NetworkSerializer<T> networkSerializer, NetworkDeserializer<T> networkDeserializer, NetworkIdentifier<T> networkIdentifier) {
public record JavaRegistryKey<T>(Key registryKey, RegistryLookup<T> lookup) {
/**
* Converts an object in this registry to its network ID.
* Converts an object to its network ID, or -1 if it is not registered.
*/
public int toNetworkId(GeyserSession session, T object) {
return networkSerializer.toNetworkId(session, this, object);
public int networkId(GeyserSession session, T object) {
return entry(session, object).map(RegistryEntryData::id).orElse(-1);
}
/**
* Converts a network ID to an object in this registry.
* Converts a registered key to its network ID, or -1 if it is not registered.
*/
public T fromNetworkId(GeyserSession session, int networkId) {
return networkDeserializer.fromNetworkId(session, this, networkId);
public int networkId(GeyserSession session, Key key) {
return entry(session, key).map(RegistryEntryData::id).orElse(-1);
}
/**
* Converts a network ID to the key it's registered under in this registry.
* Converts an object to its registered key, or null if it is not registered.
*/
public Key keyFromNetworkId(GeyserSession session, int networkId) {
return networkIdentifier.keyFromNetworkId(session, this, networkId);
public @Nullable Key key(GeyserSession session, T object) {
return entry(session, object).map(RegistryEntryData::key).orElse(null);
}
@FunctionalInterface
public interface NetworkSerializer<T> {
int toNetworkId(GeyserSession session, JavaRegistryKey<T> registry, T object);
/**
* Converts a network ID to its registered key, or null if it is not registered.
*/
public @Nullable Key key(GeyserSession session, int networkId) {
return entry(session, networkId).map(RegistryEntryData::key).orElse(null);
}
@FunctionalInterface
public interface NetworkDeserializer<T> {
T fromNetworkId(GeyserSession session, JavaRegistryKey<T> registry, int networkId);
/**
* Converts a network ID to an object in this registry, or null if it is not registered.
*/
public @Nullable T value(GeyserSession session, int networkId) {
return entry(session, networkId).map(RegistryEntryData::data).orElse(null);
}
@FunctionalInterface
public interface NetworkIdentifier<T> {
/**
* Converts a key to an object in this registry, or null if it is not registered.
*/
public @Nullable T value(GeyserSession session, Key key) {
return entry(session, key).map(RegistryEntryData::data).orElse(null);
}
Key keyFromNetworkId(GeyserSession session, JavaRegistryKey<T> registry, int networkId);
private Optional<RegistryEntryData<T>> entry(GeyserSession session, T object) {
return lookup.entry(session, this, object);
}
private Optional<RegistryEntryData<T>> entry(GeyserSession session, int networkId) {
return lookup.entry(session, this, networkId);
}
private Optional<RegistryEntryData<T>> entry(GeyserSession session, Key key) {
return lookup.entry(session, this, key);
}
/**
* Implementations should look up an element in the given registry by its value, network ID, or registered key. Return an empty optional if it does not exist.
*/
public interface RegistryLookup<T> {
Optional<RegistryEntryData<T>> entry(GeyserSession session, JavaRegistryKey<T> registry, int networkId);
Optional<RegistryEntryData<T>> entry(GeyserSession session, JavaRegistryKey<T> registry, Key key);
Optional<RegistryEntryData<T>> entry(GeyserSession session, JavaRegistryKey<T> registry, T object);
}
@Override
public @NonNull String toString() {
return "Java registry: " + registryKey;
}
}

View File

@@ -41,8 +41,11 @@ import org.geysermc.mcprotocollib.protocol.data.game.RegistryEntry;
*/
public record RegistryEntryContext(RegistryEntry entry, Object2IntMap<Key> keyIdMap, GeyserSession session) {
// TODO: not a fan of this. With JavaRegistryKey#key now being a thing, I'd rather have that always used, so that registry readers won't have to worry
// about using the right method. This would require pre-populating all data-driven registries with default (probably null) values before actually decoding the data from the registy packet.
// This could also be helpful in the feature when a data-driven registry reader needs to use an element from another data-driven registry
public int getNetworkId(Key registryKey) {
return keyIdMap.getOrDefault(registryKey, 0);
return keyIdMap.getOrDefault(registryKey, -1);
}
public Key id() {

View File

@@ -27,5 +27,5 @@ package org.geysermc.geyser.session.cache.registry;
import net.kyori.adventure.key.Key;
public record RegistryEntryData<T>(Key key, T data) {
public record RegistryEntryData<T>(int id, Key key, T data) {
}

View File

@@ -28,6 +28,7 @@ package org.geysermc.geyser.session.cache.registry;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import net.kyori.adventure.key.Key;
import org.checkerframework.checker.index.qual.NonNegative;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.util.List;
@@ -42,6 +43,14 @@ public class SimpleJavaRegistry<T> implements JavaRegistry<T> {
return this.values.get(id).data();
}
@Override
public RegistryEntryData<T> entryById(@NonNegative int id) {
if (id < 0 || id >= this.values.size()) {
return null;
}
return this.values.get(id);
}
@Override
public T byKey(Key key) {
for (RegistryEntryData<T> entry : values) {
@@ -53,11 +62,13 @@ public class SimpleJavaRegistry<T> implements JavaRegistry<T> {
}
@Override
public RegistryEntryData<T> entryById(@NonNegative int id) {
if (id < 0 || id >= this.values.size()) {
return null;
public @Nullable RegistryEntryData<T> entryByKey(Key key) {
for (RegistryEntryData<T> entry : values) {
if (entry.key().equals(key)) {
return entry;
}
return this.values.get(id);
}
return null;
}
@Override

View File

@@ -42,48 +42,45 @@ public final class BlockTag {
public static final Tag<Block> BUTTONS = create("buttons");
public static final Tag<Block> WOOL_CARPETS = create("wool_carpets");
public static final Tag<Block> WOODEN_DOORS = create("wooden_doors");
public static final Tag<Block> MOB_INTERACTABLE_DOORS = create("mob_interactable_doors");
public static final Tag<Block> WOODEN_STAIRS = create("wooden_stairs");
public static final Tag<Block> WOODEN_SLABS = create("wooden_slabs");
public static final Tag<Block> WOODEN_FENCES = create("wooden_fences");
public static final Tag<Block> PRESSURE_PLATES = create("pressure_plates");
public static final Tag<Block> FENCE_GATES = create("fence_gates");
public static final Tag<Block> WOODEN_PRESSURE_PLATES = create("wooden_pressure_plates");
public static final Tag<Block> STONE_PRESSURE_PLATES = create("stone_pressure_plates");
public static final Tag<Block> WOODEN_TRAPDOORS = create("wooden_trapdoors");
public static final Tag<Block> DOORS = create("doors");
public static final Tag<Block> SAPLINGS = create("saplings");
public static final Tag<Block> LOGS_THAT_BURN = create("logs_that_burn");
public static final Tag<Block> OVERWORLD_NATURAL_LOGS = create("overworld_natural_logs");
public static final Tag<Block> LOGS = create("logs");
public static final Tag<Block> BAMBOO_BLOCKS = create("bamboo_blocks");
public static final Tag<Block> OAK_LOGS = create("oak_logs");
public static final Tag<Block> DARK_OAK_LOGS = create("dark_oak_logs");
public static final Tag<Block> PALE_OAK_LOGS = create("pale_oak_logs");
public static final Tag<Block> OAK_LOGS = create("oak_logs");
public static final Tag<Block> BIRCH_LOGS = create("birch_logs");
public static final Tag<Block> ACACIA_LOGS = create("acacia_logs");
public static final Tag<Block> CHERRY_LOGS = create("cherry_logs");
public static final Tag<Block> JUNGLE_LOGS = create("jungle_logs");
public static final Tag<Block> SPRUCE_LOGS = create("spruce_logs");
public static final Tag<Block> MANGROVE_LOGS = create("mangrove_logs");
public static final Tag<Block> JUNGLE_LOGS = create("jungle_logs");
public static final Tag<Block> CHERRY_LOGS = create("cherry_logs");
public static final Tag<Block> CRIMSON_STEMS = create("crimson_stems");
public static final Tag<Block> WARPED_STEMS = create("warped_stems");
public static final Tag<Block> BAMBOO_BLOCKS = create("bamboo_blocks");
public static final Tag<Block> WART_BLOCKS = create("wart_blocks");
public static final Tag<Block> BANNERS = create("banners");
public static final Tag<Block> LOGS_THAT_BURN = create("logs_that_burn");
public static final Tag<Block> LOGS = create("logs");
public static final Tag<Block> SAND = create("sand");
public static final Tag<Block> SMELTS_TO_GLASS = create("smelts_to_glass");
public static final Tag<Block> STAIRS = create("stairs");
public static final Tag<Block> SLABS = create("slabs");
public static final Tag<Block> WALLS = create("walls");
public static final Tag<Block> STAIRS = create("stairs");
public static final Tag<Block> ANVIL = create("anvil");
public static final Tag<Block> RAILS = create("rails");
public static final Tag<Block> LEAVES = create("leaves");
public static final Tag<Block> WOODEN_TRAPDOORS = create("wooden_trapdoors");
public static final Tag<Block> TRAPDOORS = create("trapdoors");
public static final Tag<Block> SMALL_FLOWERS = create("small_flowers");
public static final Tag<Block> FLOWERS = create("flowers");
public static final Tag<Block> BEDS = create("beds");
public static final Tag<Block> FENCES = create("fences");
public static final Tag<Block> FLOWERS = create("flowers");
public static final Tag<Block> BEE_ATTRACTIVE = create("bee_attractive");
public static final Tag<Block> PIGLIN_REPELLENTS = create("piglin_repellents");
public static final Tag<Block> SOUL_FIRE_BASE_BLOCKS = create("soul_fire_base_blocks");
public static final Tag<Block> CANDLES = create("candles");
public static final Tag<Block> DAMPENS_VIBRATIONS = create("dampens_vibrations");
public static final Tag<Block> GOLD_ORES = create("gold_ores");
public static final Tag<Block> IRON_ORES = create("iron_ores");
public static final Tag<Block> DIAMOND_ORES = create("diamond_ores");
@@ -92,13 +89,21 @@ public final class BlockTag {
public static final Tag<Block> COAL_ORES = create("coal_ores");
public static final Tag<Block> EMERALD_ORES = create("emerald_ores");
public static final Tag<Block> COPPER_ORES = create("copper_ores");
public static final Tag<Block> CANDLES = create("candles");
public static final Tag<Block> DIRT = create("dirt");
public static final Tag<Block> TERRACOTTA = create("terracotta");
public static final Tag<Block> BADLANDS_TERRACOTTA = create("badlands_terracotta");
public static final Tag<Block> CONCRETE_POWDER = create("concrete_powder");
public static final Tag<Block> COMPLETES_FIND_TREE_TUTORIAL = create("completes_find_tree_tutorial");
public static final Tag<Block> SHULKER_BOXES = create("shulker_boxes");
public static final Tag<Block> CEILING_HANGING_SIGNS = create("ceiling_hanging_signs");
public static final Tag<Block> STANDING_SIGNS = create("standing_signs");
public static final Tag<Block> BEE_ATTRACTIVE = create("bee_attractive");
public static final Tag<Block> MOB_INTERACTABLE_DOORS = create("mob_interactable_doors");
public static final Tag<Block> PRESSURE_PLATES = create("pressure_plates");
public static final Tag<Block> STONE_PRESSURE_PLATES = create("stone_pressure_plates");
public static final Tag<Block> OVERWORLD_NATURAL_LOGS = create("overworld_natural_logs");
public static final Tag<Block> BANNERS = create("banners");
public static final Tag<Block> PIGLIN_REPELLENTS = create("piglin_repellents");
public static final Tag<Block> BADLANDS_TERRACOTTA = create("badlands_terracotta");
public static final Tag<Block> CONCRETE_POWDER = create("concrete_powder");
public static final Tag<Block> FLOWER_POTS = create("flower_pots");
public static final Tag<Block> ENDERMAN_HOLDABLE = create("enderman_holdable");
public static final Tag<Block> ICE = create("ice");
@@ -110,10 +115,8 @@ public final class BlockTag {
public static final Tag<Block> CORAL_PLANTS = create("coral_plants");
public static final Tag<Block> CORALS = create("corals");
public static final Tag<Block> BAMBOO_PLANTABLE_ON = create("bamboo_plantable_on");
public static final Tag<Block> STANDING_SIGNS = create("standing_signs");
public static final Tag<Block> WALL_SIGNS = create("wall_signs");
public static final Tag<Block> SIGNS = create("signs");
public static final Tag<Block> CEILING_HANGING_SIGNS = create("ceiling_hanging_signs");
public static final Tag<Block> WALL_HANGING_SIGNS = create("wall_hanging_signs");
public static final Tag<Block> ALL_HANGING_SIGNS = create("all_hanging_signs");
public static final Tag<Block> ALL_SIGNS = create("all_signs");
@@ -133,12 +136,10 @@ public final class BlockTag {
public static final Tag<Block> CLIMBABLE = create("climbable");
public static final Tag<Block> FALL_DAMAGE_RESETTING = create("fall_damage_resetting");
public static final Tag<Block> HOGLIN_REPELLENTS = create("hoglin_repellents");
public static final Tag<Block> SOUL_FIRE_BASE_BLOCKS = create("soul_fire_base_blocks");
public static final Tag<Block> STRIDER_WARM_BLOCKS = create("strider_warm_blocks");
public static final Tag<Block> CAMPFIRES = create("campfires");
public static final Tag<Block> GUARDED_BY_PIGLINS = create("guarded_by_piglins");
public static final Tag<Block> PREVENT_MOB_SPAWNING_INSIDE = create("prevent_mob_spawning_inside");
public static final Tag<Block> FENCE_GATES = create("fence_gates");
public static final Tag<Block> UNSTABLE_BOTTOM_CENTER = create("unstable_bottom_center");
public static final Tag<Block> MUSHROOM_GROW_BLOCK = create("mushroom_grow_block");
public static final Tag<Block> EDIBLE_FOR_SHEEP = create("edible_for_sheep");
@@ -157,8 +158,8 @@ public final class BlockTag {
public static final Tag<Block> INSIDE_STEP_SOUND_BLOCKS = create("inside_step_sound_blocks");
public static final Tag<Block> COMBINATION_STEP_SOUND_BLOCKS = create("combination_step_sound_blocks");
public static final Tag<Block> CAMEL_SAND_STEP_SOUND_BLOCKS = create("camel_sand_step_sound_blocks");
public static final Tag<Block> HAPPY_GHAST_AVOIDS = create("happy_ghast_avoids");
public static final Tag<Block> OCCLUDES_VIBRATION_SIGNALS = create("occludes_vibration_signals");
public static final Tag<Block> DAMPENS_VIBRATIONS = create("dampens_vibrations");
public static final Tag<Block> DRIPSTONE_REPLACEABLE_BLOCKS = create("dripstone_replaceable_blocks");
public static final Tag<Block> CAVE_VINES = create("cave_vines");
public static final Tag<Block> MOSS_REPLACEABLE = create("moss_replaceable");
@@ -223,7 +224,9 @@ public final class BlockTag {
public static final Tag<Block> MAINTAINS_FARMLAND = create("maintains_farmland");
public static final Tag<Block> BLOCKS_WIND_CHARGE_EXPLOSIONS = create("blocks_wind_charge_explosions");
public static final Tag<Block> DOES_NOT_BLOCK_HOPPERS = create("does_not_block_hoppers");
public static final Tag<Block> PLAYS_AMBIENT_DESERT_BLOCK_SOUNDS = create("plays_ambient_desert_block_sounds");
public static final Tag<Block> TRIGGERS_AMBIENT_DESERT_SAND_BLOCK_SOUNDS = create("triggers_ambient_desert_sand_block_sounds");
public static final Tag<Block> TRIGGERS_AMBIENT_DESERT_DRY_VEGETATION_BLOCK_SOUNDS = create("triggers_ambient_desert_dry_vegetation_block_sounds");
public static final Tag<Block> TRIGGERS_AMBIENT_DRIED_GHAST_BLOCK_SOUNDS = create("triggers_ambient_dried_ghast_block_sounds");
public static final Tag<Block> AIR = create("air");
private BlockTag() {}

View File

@@ -0,0 +1,41 @@
/*
* 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.session.cache.tags;
import org.geysermc.geyser.session.cache.registry.JavaRegistries;
import org.geysermc.geyser.session.dialog.Dialog;
import org.geysermc.geyser.util.MinecraftKey;
public final class DialogTag {
public static final Tag<Dialog> PAUSE_SCREEN_ADDITIONS = create("pause_screen_additions");
public static final Tag<Dialog> QUICK_ACTIONS = create("quick_actions");
private DialogTag() {}
private static Tag<Dialog> create(String name) {
return new Tag<>(JavaRegistries.DIALOG, MinecraftKey.key(name));
}
}

View File

@@ -30,22 +30,30 @@ import lombok.Data;
import net.kyori.adventure.key.Key;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.nbt.NbtMap;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.TagCache;
import org.geysermc.geyser.session.cache.registry.JavaRegistryKey;
import org.geysermc.geyser.util.MinecraftKey;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.HolderSet;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
import java.util.function.ToIntFunction;
/**
* Similar to vanilla Minecraft's HolderSets, stores either a tag or a list of IDs (this list can also be represented as a single ID in vanilla HolderSets).
* Similar to vanilla Minecraft's HolderSets, stores either a tag, a list of IDs (this list can also be represented as a single ID in vanilla HolderSets),
* or a list of inline elements (only supported by some HolderSets, and can also be represented as a single inline element in vanilla HolderSets).
*
* <p>Because HolderSets utilise tags, when loading a HolderSet, Geyser must store tags for the registry the HolderSet is for (see {@link JavaRegistryKey}).</p>
* <p>Because HolderSets utilise tags, when loading a HolderSet, Geyser must store tags for the registry the HolderSet is for. This is done for all registries registered in
* {@link org.geysermc.geyser.session.cache.registry.JavaRegistries}.</p>
*
* <p>Use the {@link GeyserHolderSet#readHolderSet} method to easily read a HolderSet from NBT sent by a server. To turn the HolderSet into a list of network IDs, use the {@link GeyserHolderSet#resolveRaw} method.
* To turn the HolderSet into a list of objects, use the {@link GeyserHolderSet#resolve} method.</p>
*
* <p>Note that the {@link GeyserHolderSet#resolveRaw(TagCache)} method will fail for inline HolderSets, since inline elements are not registered and as such have no network ID.</p>
*/
@Data
public final class GeyserHolderSet<T> {
@@ -53,45 +61,65 @@ public final class GeyserHolderSet<T> {
private final JavaRegistryKey<T> registry;
private final @Nullable Tag<T> tag;
private final int @Nullable [] holders;
private final @Nullable List<T> inline;
private GeyserHolderSet(JavaRegistryKey<T> registry) {
this(registry, IntArrays.EMPTY_ARRAY);
}
public GeyserHolderSet(JavaRegistryKey<T> registry, int @NonNull [] holders) {
this(registry, null, holders);
this(registry, null, holders, null);
}
public GeyserHolderSet(JavaRegistryKey<T> registry, @NonNull Tag<T> tagId) {
this(registry, tagId, null);
this(registry, tagId, null, null);
}
private GeyserHolderSet(JavaRegistryKey<T> registry, @Nullable Tag<T> tag, int @Nullable [] holders) {
public GeyserHolderSet(JavaRegistryKey<T> registry, @NonNull List<T> inline) {
this(registry, null, null, inline);
}
private GeyserHolderSet(JavaRegistryKey<T> registry, @Nullable Tag<T> tag, int @Nullable [] holders, @Nullable List<T> inline) {
this.registry = registry;
this.tag = tag;
this.holders = holders;
this.inline = inline;
}
/**
* Constructs a {@link GeyserHolderSet} from a MCPL HolderSet.
*/
public static <T> GeyserHolderSet<T> fromHolderSet(JavaRegistryKey<T> registry, @NonNull HolderSet holderSet) {
// MCPL HolderSets don't have to support inline elements... for now (TODO CHECK ME)
Tag<T> tag = holderSet.getLocation() == null ? null : new Tag<>(registry, holderSet.getLocation());
return new GeyserHolderSet<>(registry, tag, holderSet.getHolders());
return new GeyserHolderSet<>(registry, tag, holderSet.getHolders(), null);
}
/**
* Resolves the HolderSet, and automatically maps the network IDs to their respective object types. If the HolderSet is a list of IDs, this will be returned. If it is a tag, the tag will be resolved from the tag cache.
* Resolves the HolderSet, and automatically maps the network IDs to their respective object types.
* If the HolderSet is a list of IDs, this will be returned. If it is a tag, the tag will be resolved from the tag cache. If it is an inline HolderSet, the list of inline elements will be returned.
*
* @return the HolderSet turned into a list of objects.
*/
public List<T> resolve(GeyserSession session) {
if (inline != null) {
return inline;
}
return TagCache.mapRawArray(session, resolveRaw(session.getTagCache()), registry);
}
/**
* Resolves the HolderSet. If the HolderSet is a list of IDs, this will be returned. If it is a tag, the tag will be resolved from the tag cache.
* Resolves the HolderSet into a list of network IDs. If the HolderSet is a list of IDs, this will be returned. If it is a tag, the tag will be resolved from the tag cache.
*
* @return the HolderSet turned into a list of objects.
* <p>If the HolderSet is a list of inline elements, this method will throw! Inline elements are not registered and as such do not have a network ID.</p>
*
* @return the HolderSet turned into a list of network IDs.
* @throws IllegalStateException when the HolderSet is a list of inline elements.
*/
public int[] resolveRaw(TagCache tagCache) {
if (holders != null) {
if (inline != null) {
throw new IllegalStateException("Tried to resolve network IDs of a GeyserHolderSet(registry=" + registry + ") with inline elements!");
} else if (holders != null) {
return holders;
}
@@ -99,31 +127,72 @@ public final class GeyserHolderSet<T> {
}
/**
* Reads a HolderSet from an object from NBT.
* Reads a HolderSet from a NBT object. Does not support reading HolderSets that can hold inline values.
*
* @param session session, only used for logging purposes.
* <p>Uses {@link JavaRegistryKey#networkId(GeyserSession, Key)} to resolve registry keys to network IDs.</p>
*
* @param session the Geyser session.
* @param registry the registry the HolderSet contains IDs from.
* @param holderSet the HolderSet as an object from NBT.
* @param keyIdMapping a function that maps resource location IDs in the HolderSet's registry to their network IDs.
* @param holderSet the HolderSet as a NBT object.
*/
public static <T> GeyserHolderSet<T> readHolderSet(GeyserSession session, JavaRegistryKey<T> registry, @Nullable Object holderSet, ToIntFunction<Key> keyIdMapping) {
if (holderSet == null) {
return new GeyserHolderSet<>(registry, IntArrays.EMPTY_ARRAY);
public static <T> GeyserHolderSet<T> readHolderSet(GeyserSession session, JavaRegistryKey<T> registry, @Nullable Object holderSet) {
return readHolderSet(registry, holderSet, key -> registry.networkId(session, key));
}
if (holderSet instanceof String stringTag) {
if (stringTag.startsWith("#")) {
/**
* Reads a HolderSet from a NBT object. Does not support reading HolderSets that can hold inline values.
*
* @param registry the registry the HolderSet contains IDs from.
* @param holderSet the HolderSet as a NBT object.
* @param idMapper a function that maps a key in this registry to its respective network ID.
*/
public static <T> GeyserHolderSet<T> readHolderSet(JavaRegistryKey<T> registry, @Nullable Object holderSet, ToIntFunction<Key> idMapper) {
return readHolderSet(registry, holderSet, idMapper, null);
}
/**
* Reads a HolderSet from a NBT object. When {@code reader} is not null, this method can read HolderSets with inline registry elements as well, using the passed reader to decode
* registry elements.
*
* @param registry the registry the HolderSet contains IDs from.
* @param holderSet the HolderSet as a NBT object.
* @param idMapper a function that maps a key in this registry to its respective network ID.
* @param reader a function that reads an object in the HolderSet's registry, serialised as NBT. When {@code null}, this method doesn't support reading inline HolderSets.
*/
public static <T> GeyserHolderSet<T> readHolderSet(JavaRegistryKey<T> registry, @Nullable Object holderSet,
ToIntFunction<Key> idMapper, @Nullable Function<NbtMap, T> reader) {
if (holderSet == null) {
return new GeyserHolderSet<>(registry);
}
// This is technically wrong, some registries might not serialise their elements as a map. However, right now this is only used for dialogs,
// so it works. If this ever changes, we'll have to accommodate for that here
if (holderSet instanceof NbtMap singleInlineElement && reader != null) {
return new GeyserHolderSet<>(registry, List.of(reader.apply(singleInlineElement)));
} if (holderSet instanceof String elementOrTag) {
if (elementOrTag.startsWith("#")) {
// Tag
return new GeyserHolderSet<>(registry, new Tag<>(registry, Key.key(stringTag.substring(1)))); // Remove '#' at beginning that indicates tag
} else if (stringTag.isEmpty()) {
return new GeyserHolderSet<>(registry, IntArrays.EMPTY_ARRAY);
return new GeyserHolderSet<>(registry, new Tag<>(registry, MinecraftKey.key(elementOrTag.substring(1)))); // Remove '#' at beginning that indicates a tag
} else if (elementOrTag.isEmpty()) {
return new GeyserHolderSet<>(registry);
}
return new GeyserHolderSet<>(registry, new int[]{keyIdMapping.applyAsInt(Key.key(stringTag))});
return new GeyserHolderSet<>(registry, new int[]{idMapper.applyAsInt(MinecraftKey.key(elementOrTag))});
} else if (holderSet instanceof List<?> list) {
// Assume the list is a list of strings
return new GeyserHolderSet<>(registry, list.stream().map(o -> (String) o).map(Key::key).mapToInt(keyIdMapping).toArray());
if (list.isEmpty()) {
return new GeyserHolderSet<>(registry);
} else if (list.get(0) instanceof NbtMap) {
if (reader != null) {
return new GeyserHolderSet<>(registry, list.stream().map(o -> (NbtMap) o).map(reader).toList());
}
session.getGeyser().getLogger().warning("Failed parsing HolderSet for registry + " + registry + "! Expected either a tag, a string ID or a list of string IDs, found " + holderSet);
return new GeyserHolderSet<>(registry, IntArrays.EMPTY_ARRAY);
} else {
// Assume the list is a list of strings (resource locations)
return new GeyserHolderSet<>(registry, list.stream().map(o -> (String) o).map(Key::key).mapToInt(idMapper).toArray());
}
}
String expected = reader == null ? "either a tag, a string ID, or a list of string IDs"
: "either a tag, a string ID, an inline registry element, a list of string IDs, or a list of inline registry elements";
GeyserImpl.getInstance().getLogger().warning("Failed parsing HolderSet for registry + " + registry + "! Expected " + expected + ", found " + holderSet);
return new GeyserHolderSet<>(registry);
}
}

View File

@@ -47,45 +47,62 @@ public final class ItemTag {
public static final Tag<Item> WOODEN_FENCES = create("wooden_fences");
public static final Tag<Item> FENCE_GATES = create("fence_gates");
public static final Tag<Item> WOODEN_PRESSURE_PLATES = create("wooden_pressure_plates");
public static final Tag<Item> WOODEN_TRAPDOORS = create("wooden_trapdoors");
public static final Tag<Item> DOORS = create("doors");
public static final Tag<Item> SAPLINGS = create("saplings");
public static final Tag<Item> LOGS_THAT_BURN = create("logs_that_burn");
public static final Tag<Item> LOGS = create("logs");
public static final Tag<Item> BAMBOO_BLOCKS = create("bamboo_blocks");
public static final Tag<Item> OAK_LOGS = create("oak_logs");
public static final Tag<Item> DARK_OAK_LOGS = create("dark_oak_logs");
public static final Tag<Item> PALE_OAK_LOGS = create("pale_oak_logs");
public static final Tag<Item> OAK_LOGS = create("oak_logs");
public static final Tag<Item> BIRCH_LOGS = create("birch_logs");
public static final Tag<Item> ACACIA_LOGS = create("acacia_logs");
public static final Tag<Item> CHERRY_LOGS = create("cherry_logs");
public static final Tag<Item> JUNGLE_LOGS = create("jungle_logs");
public static final Tag<Item> SPRUCE_LOGS = create("spruce_logs");
public static final Tag<Item> MANGROVE_LOGS = create("mangrove_logs");
public static final Tag<Item> JUNGLE_LOGS = create("jungle_logs");
public static final Tag<Item> CHERRY_LOGS = create("cherry_logs");
public static final Tag<Item> CRIMSON_STEMS = create("crimson_stems");
public static final Tag<Item> WARPED_STEMS = create("warped_stems");
public static final Tag<Item> BAMBOO_BLOCKS = create("bamboo_blocks");
public static final Tag<Item> WART_BLOCKS = create("wart_blocks");
public static final Tag<Item> BANNERS = create("banners");
public static final Tag<Item> LOGS_THAT_BURN = create("logs_that_burn");
public static final Tag<Item> LOGS = create("logs");
public static final Tag<Item> SAND = create("sand");
public static final Tag<Item> SMELTS_TO_GLASS = create("smelts_to_glass");
public static final Tag<Item> STAIRS = create("stairs");
public static final Tag<Item> SLABS = create("slabs");
public static final Tag<Item> WALLS = create("walls");
public static final Tag<Item> STAIRS = create("stairs");
public static final Tag<Item> ANVIL = create("anvil");
public static final Tag<Item> RAILS = create("rails");
public static final Tag<Item> LEAVES = create("leaves");
public static final Tag<Item> WOODEN_TRAPDOORS = create("wooden_trapdoors");
public static final Tag<Item> TRAPDOORS = create("trapdoors");
public static final Tag<Item> SMALL_FLOWERS = create("small_flowers");
public static final Tag<Item> FLOWERS = create("flowers");
public static final Tag<Item> BEDS = create("beds");
public static final Tag<Item> FENCES = create("fences");
public static final Tag<Item> SOUL_FIRE_BASE_BLOCKS = create("soul_fire_base_blocks");
public static final Tag<Item> CANDLES = create("candles");
public static final Tag<Item> DAMPENS_VIBRATIONS = create("dampens_vibrations");
public static final Tag<Item> GOLD_ORES = create("gold_ores");
public static final Tag<Item> IRON_ORES = create("iron_ores");
public static final Tag<Item> DIAMOND_ORES = create("diamond_ores");
public static final Tag<Item> REDSTONE_ORES = create("redstone_ores");
public static final Tag<Item> LAPIS_ORES = create("lapis_ores");
public static final Tag<Item> COAL_ORES = create("coal_ores");
public static final Tag<Item> EMERALD_ORES = create("emerald_ores");
public static final Tag<Item> COPPER_ORES = create("copper_ores");
public static final Tag<Item> DIRT = create("dirt");
public static final Tag<Item> TERRACOTTA = create("terracotta");
public static final Tag<Item> COMPLETES_FIND_TREE_TUTORIAL = create("completes_find_tree_tutorial");
public static final Tag<Item> SHULKER_BOXES = create("shulker_boxes");
public static final Tag<Item> SIGNS = create("signs");
public static final Tag<Item> HANGING_SIGNS = create("hanging_signs");
public static final Tag<Item> BEE_FOOD = create("bee_food");
public static final Tag<Item> BANNERS = create("banners");
public static final Tag<Item> PIGLIN_REPELLENTS = create("piglin_repellents");
public static final Tag<Item> PIGLIN_LOVED = create("piglin_loved");
public static final Tag<Item> IGNORED_BY_PIGLIN_BABIES = create("ignored_by_piglin_babies");
public static final Tag<Item> PIGLIN_SAFE_ARMOR = create("piglin_safe_armor");
public static final Tag<Item> DUPLICATES_ALLAYS = create("duplicates_allays");
public static final Tag<Item> BREWING_FUEL = create("brewing_fuel");
public static final Tag<Item> SHULKER_BOXES = create("shulker_boxes");
public static final Tag<Item> EGGS = create("eggs");
public static final Tag<Item> MEAT = create("meat");
public static final Tag<Item> SNIFFER_FOOD = create("sniffer_food");
@@ -98,9 +115,11 @@ public final class ItemTag {
public static final Tag<Item> CAT_FOOD = create("cat_food");
public static final Tag<Item> HORSE_FOOD = create("horse_food");
public static final Tag<Item> HORSE_TEMPT_ITEMS = create("horse_tempt_items");
public static final Tag<Item> HARNESSES = create("harnesses");
public static final Tag<Item> HAPPY_GHAST_FOOD = create("happy_ghast_food");
public static final Tag<Item> HAPPY_GHAST_TEMPT_ITEMS = create("happy_ghast_tempt_items");
public static final Tag<Item> CAMEL_FOOD = create("camel_food");
public static final Tag<Item> ARMADILLO_FOOD = create("armadillo_food");
public static final Tag<Item> BEE_FOOD = create("bee_food");
public static final Tag<Item> CHICKEN_FOOD = create("chicken_food");
public static final Tag<Item> FROG_FOOD = create("frog_food");
public static final Tag<Item> HOGLIN_FOOD = create("hoglin_food");
@@ -117,24 +136,10 @@ public final class ItemTag {
public static final Tag<Item> PARROT_FOOD = create("parrot_food");
public static final Tag<Item> PARROT_POISONOUS_FOOD = create("parrot_poisonous_food");
public static final Tag<Item> AXOLOTL_FOOD = create("axolotl_food");
public static final Tag<Item> GOLD_ORES = create("gold_ores");
public static final Tag<Item> IRON_ORES = create("iron_ores");
public static final Tag<Item> DIAMOND_ORES = create("diamond_ores");
public static final Tag<Item> REDSTONE_ORES = create("redstone_ores");
public static final Tag<Item> LAPIS_ORES = create("lapis_ores");
public static final Tag<Item> COAL_ORES = create("coal_ores");
public static final Tag<Item> EMERALD_ORES = create("emerald_ores");
public static final Tag<Item> COPPER_ORES = create("copper_ores");
public static final Tag<Item> NON_FLAMMABLE_WOOD = create("non_flammable_wood");
public static final Tag<Item> SOUL_FIRE_BASE_BLOCKS = create("soul_fire_base_blocks");
public static final Tag<Item> CANDLES = create("candles");
public static final Tag<Item> DIRT = create("dirt");
public static final Tag<Item> TERRACOTTA = create("terracotta");
public static final Tag<Item> COMPLETES_FIND_TREE_TUTORIAL = create("completes_find_tree_tutorial");
public static final Tag<Item> BOATS = create("boats");
public static final Tag<Item> CHEST_BOATS = create("chest_boats");
public static final Tag<Item> FISHES = create("fishes");
public static final Tag<Item> SIGNS = create("signs");
public static final Tag<Item> CREEPER_DROP_MUSIC_DISCS = create("creeper_drop_music_discs");
public static final Tag<Item> COALS = create("coals");
public static final Tag<Item> ARROWS = create("arrows");
@@ -157,10 +162,8 @@ public final class ItemTag {
public static final Tag<Item> REPAIRS_WOLF_ARMOR = create("repairs_wolf_armor");
public static final Tag<Item> STONE_CRAFTING_MATERIALS = create("stone_crafting_materials");
public static final Tag<Item> FREEZE_IMMUNE_WEARABLES = create("freeze_immune_wearables");
public static final Tag<Item> DAMPENS_VIBRATIONS = create("dampens_vibrations");
public static final Tag<Item> CLUSTER_MAX_HARVESTABLES = create("cluster_max_harvestables");
public static final Tag<Item> COMPASSES = create("compasses");
public static final Tag<Item> HANGING_SIGNS = create("hanging_signs");
public static final Tag<Item> CREEPER_IGNITERS = create("creeper_igniters");
public static final Tag<Item> NOTEBLOCK_TOP_INSTRUMENTS = create("noteblock_top_instruments");
public static final Tag<Item> FOOT_ARMOR = create("foot_armor");

View File

@@ -0,0 +1,72 @@
/*
* 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.session.cache.waypoint;
import org.cloudburstmc.math.vector.Vector3f;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.mcprotocollib.protocol.data.game.level.waypoint.AzimuthWaypointData;
import org.geysermc.mcprotocollib.protocol.data.game.level.waypoint.WaypointData;
import java.awt.Color;
import java.util.Optional;
import java.util.OptionalLong;
import java.util.UUID;
public class AzimuthWaypoint extends GeyserWaypoint implements TickingWaypoint {
// In Java, this waypoint always appears really far, so set the distance far here too,
// This also makes the waypoint more accurate on the bar and less susceptible to the player moving
private static final float WAYPOINT_DISTANCE = 1000.0F;
// The angle, in radians, where the waypoint should appear on the bar
private float angle = 0.0F;
public AzimuthWaypoint(GeyserSession session, Optional<UUID> uuid, OptionalLong entityId, Color color) {
super(session, uuid, entityId, color);
}
@Override
public void setData(WaypointData data) {
angle = ((AzimuthWaypointData) data).angle();
updatePosition();
}
@Override
public void tick() {
// Update position so that it remains accurate to the angle as the player moves around
updatePosition();
sendLocationPacket(false);
}
private void updatePosition() {
Vector3f playerPosition = session.getPlayerEntity().position();
// Unit circle math!
float dx = (float) (Math.cos(angle) * WAYPOINT_DISTANCE);
float dz = (float) -(Math.sin(angle) * WAYPOINT_DISTANCE);
// Set Y to the player's Y since this waypoint always appears in the centre of the bar on Java
position = Vector3f.from(playerPosition.getX() + dx, playerPosition.getY(), playerPosition.getZ() + dz);
}
}

View File

@@ -0,0 +1,50 @@
/*
* 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.session.cache.waypoint;
import org.cloudburstmc.math.vector.Vector3f;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.mcprotocollib.protocol.data.game.level.waypoint.ChunkWaypointData;
import org.geysermc.mcprotocollib.protocol.data.game.level.waypoint.WaypointData;
import java.awt.Color;
import java.util.Optional;
import java.util.OptionalLong;
import java.util.UUID;
public class ChunkWaypoint extends GeyserWaypoint {
public ChunkWaypoint(GeyserSession session, Optional<UUID> uuid, OptionalLong entityId, Color color) {
super(session, uuid, entityId, color);
}
@Override
public void setData(WaypointData data) {
ChunkWaypointData chunk = (ChunkWaypointData) data;
// Set position in centre of chunk
position = Vector3f.from(chunk.chunkX() * 16.0F + 8.0F, session.getPlayerEntity().position().getY(), chunk.chunkZ() * 16.0F + 8.0F);
}
}

View File

@@ -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.session.cache.waypoint;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.mcprotocollib.protocol.data.game.level.waypoint.Vec3iWaypointData;
import org.geysermc.mcprotocollib.protocol.data.game.level.waypoint.WaypointData;
import java.awt.Color;
import java.util.Optional;
import java.util.OptionalLong;
import java.util.UUID;
public class CoordinatesWaypoint extends GeyserWaypoint {
public CoordinatesWaypoint(GeyserSession session, Optional<UUID> uuid, OptionalLong entityId, Color color) {
super(session, uuid, entityId, color);
}
@Override
public void setData(WaypointData data) {
position = ((Vec3iWaypointData) data).vector().toFloat();
}
}

View File

@@ -0,0 +1,145 @@
/*
* 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.session.cache.waypoint;
import lombok.Getter;
import lombok.experimental.Accessors;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.math.vector.Vector3f;
import org.cloudburstmc.protocol.bedrock.packet.PlayerListPacket;
import org.cloudburstmc.protocol.bedrock.packet.PlayerLocationPacket;
import org.geysermc.geyser.entity.type.player.PlayerEntity;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.mcprotocollib.protocol.data.game.level.waypoint.TrackedWaypoint;
import org.geysermc.mcprotocollib.protocol.data.game.level.waypoint.WaypointData;
import java.awt.Color;
import java.util.Optional;
import java.util.OptionalLong;
import java.util.UUID;
@Accessors(fluent = true)
public abstract class GeyserWaypoint {
protected final GeyserSession session;
@Getter
private final Color color;
private final UUID entityUuid;
private long entityId;
private boolean sendListPackets;
protected Vector3f position = Vector3f.ZERO;
private Vector3f lastSent = null;
public GeyserWaypoint(GeyserSession session, Optional<UUID> uuid, OptionalLong entityId, Color color) {
this.session = session;
this.color = color;
this.entityUuid = uuid.orElseGet(UUID::randomUUID);
this.entityId = entityId.orElseGet(() -> session.getEntityCache().getNextEntityId().incrementAndGet());
this.sendListPackets = entityId.isEmpty();
}
public void track(WaypointData data) {
sendListPackets(PlayerListPacket.Action.ADD);
update(data);
}
public void update(WaypointData data) {
setData(data);
sendLocationPacket(false);
}
public void untrack() {
PlayerLocationPacket packet = new PlayerLocationPacket();
packet.setType(PlayerLocationPacket.Type.HIDE);
packet.setTargetEntityId(entityId);
session.sendUpstreamPacket(packet);
sendListPackets(PlayerListPacket.Action.REMOVE);
}
public void setPlayer(PlayerEntity entity) {
if (sendListPackets) {
untrack();
entityId = entity.getGeyserId();
sendListPackets = false;
sendLocationPacket(true);
} else if (entity == null) { // Previously had an attached player, and now that player is gone
entityId = session.getEntityCache().getNextEntityId().incrementAndGet();
sendListPackets = true;
sendListPackets(PlayerListPacket.Action.ADD);
sendLocationPacket(true);
}
}
protected void sendLocationPacket(boolean force) {
if (force || lastSent == null || position.distanceSquared(lastSent) > 1.0F) {
PlayerLocationPacket packet = new PlayerLocationPacket();
packet.setType(PlayerLocationPacket.Type.COORDINATES);
packet.setTargetEntityId(entityId);
packet.setPosition(position);
session.sendUpstreamPacket(packet);
lastSent = position;
}
}
private void sendListPackets(PlayerListPacket.Action action) {
if (sendListPackets) {
PlayerListPacket packet = new PlayerListPacket();
packet.setAction(action);
PlayerListPacket.Entry entry = new PlayerListPacket.Entry(entityUuid);
entry.setEntityId(entityId);
entry.setColor(color);
packet.getEntries().add(entry);
session.sendUpstreamPacket(packet);
}
}
public abstract void setData(WaypointData data);
public static @Nullable GeyserWaypoint create(GeyserSession session, Optional<UUID> uuid, OptionalLong entityId, TrackedWaypoint waypoint) {
Color color = getWaypointColor(waypoint);
return switch (waypoint.type()) {
case EMPTY -> null;
case VEC3I -> new CoordinatesWaypoint(session, uuid, entityId, color);
case CHUNK -> new ChunkWaypoint(session, uuid, entityId, color);
case AZIMUTH -> new AzimuthWaypoint(session, uuid, entityId, color);
};
}
private static Color getWaypointColor(TrackedWaypoint waypoint) {
// Use icon's colour, or calculate from UUID/ID if it is not specified
// This is similar to how Java does it, but they do some brightness modifications too, which is a lot of math (see LocatorBarRenderer)
return waypoint.icon().color()
.or(() -> Optional.ofNullable(waypoint.uuid()).map(UUID::hashCode))
.or(() -> Optional.ofNullable(waypoint.id()).map(String::hashCode))
.map(i -> new Color(i & 0xFFFFFF))
.orElseThrow();
}
}

View File

@@ -0,0 +1,31 @@
/*
* 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.session.cache.waypoint;
public interface TickingWaypoint {
void tick();
}

View File

@@ -0,0 +1,183 @@
/*
* 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.session.cache.waypoint;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import org.cloudburstmc.protocol.bedrock.packet.PlayerListPacket;
import org.cloudburstmc.protocol.bedrock.packet.PlayerLocationPacket;
import org.geysermc.geyser.entity.type.player.PlayerEntity;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.skin.SkinManager;
import org.geysermc.mcprotocollib.protocol.data.game.level.waypoint.TrackedWaypoint;
import org.geysermc.mcprotocollib.protocol.data.game.level.waypoint.WaypointOperation;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.level.ClientboundTrackedWaypointPacket;
import java.awt.Color;
import java.util.Map;
import java.util.Optional;
import java.util.OptionalLong;
import java.util.UUID;
public final class WaypointCache {
private final GeyserSession session;
private final Map<String, GeyserWaypoint> waypoints = new Object2ObjectOpenHashMap<>();
private final Map<UUID, Color> waypointColors = new Object2ObjectOpenHashMap<>();
public WaypointCache(GeyserSession session) {
this.session = session;
}
public void handlePacket(ClientboundTrackedWaypointPacket packet) {
switch (packet.getOperation()) {
case TRACK -> track(packet.getWaypoint());
case UNTRACK -> untrack(packet.getWaypoint());
case UPDATE -> update(packet.getWaypoint());
}
if (packet.getOperation() == WaypointOperation.TRACK || packet.getOperation()== WaypointOperation.UNTRACK) {
// Only show locator bar when there are waypoints on it
// This is equivalent to Java, and the Java locatorBar game rule won't work otherwise
session.sendGameRule("locatorBar", !waypoints.isEmpty());
}
}
public void listPlayer(PlayerEntity player) {
GeyserWaypoint waypoint = waypoints.get(player.getUuid().toString());
if (waypoint != null) {
// This will remove the fake player packet previously sent to the client,
// and change the waypoint to use the player's entity ID instead.
// This is important because sometimes a waypoint is sent before player info telling us to list the player, so a fake player packet is sent to the client
// When the player becomes listed the right colour will already be used, this is always put in the colours map, no matter if the
// player info existed or not
waypoint.setPlayer(player);
} else {
// If we haven't received a waypoint for the player, we need to tell the client to hide them
// Bedrock likes to create their own waypoints for players in render distance, but Java doesn't do this, and we don't want this either, since it could
// lead to duplicate/wrong waypoints on the locator bar
// For example, if a Java server hides a player from the locator bar even when they're not sneaking, bedrock will still show them when in render
// distance
PlayerLocationPacket locationPacket = new PlayerLocationPacket();
locationPacket.setType(PlayerLocationPacket.Type.HIDE);
locationPacket.setTargetEntityId(player.getGeyserId());
session.sendUpstreamPacket(locationPacket);
}
}
public void unlistPlayer(PlayerEntity player) {
GeyserWaypoint waypoint = waypoints.get(player.getUuid().toString());
if (waypoint != null) {
// This will remove the player packet previously sent to the client,
// and change the waypoint to use the player's entity ID instead.
// This is important because a player waypoint can still show even when a player becomes unlisted,
// so a fake player packet has to be sent to the client now
waypoint.setPlayer(null);
}
}
public Optional<Color> getWaypointColor(UUID uuid) {
return Optional.ofNullable(waypointColors.get(uuid));
}
public void tick() {
for (GeyserWaypoint waypoint : waypoints.values()) {
if (waypoint instanceof TickingWaypoint ticking) {
ticking.tick();
}
}
}
private void track(TrackedWaypoint waypoint) {
untrack(waypoint);
Optional<UUID> uuid = Optional.ofNullable(waypoint.uuid());
Optional<PlayerEntity> player = uuid.flatMap(id -> Optional.ofNullable(session.getEntityCache().getPlayerEntity(id)));
OptionalLong playerId = player.stream().mapToLong(PlayerEntity::getGeyserId).findFirst();
GeyserWaypoint tracked = GeyserWaypoint.create(session, uuid, playerId, waypoint);
if (tracked != null) {
uuid.ifPresent(id -> waypointColors.put(id, tracked.color()));
// Resend player entry with new waypoint colour
player.ifPresent(this::updatePlayerEntry);
tracked.track(waypoint.data());
waypoints.put(waypointId(waypoint), tracked);
} else {
playerId.ifPresent(id -> {
// When tracked waypoint is null, the waypoint shouldn't show up on the locator bar (Java type is EMPTY)
// If this waypoint is linked to a player, tell the bedrock client to hide it
// If we don't do this bedrock will show the waypoint anyway when the player is in render distance (read comments above in trackPlayer)
PlayerLocationPacket locationPacket = new PlayerLocationPacket();
locationPacket.setType(PlayerLocationPacket.Type.HIDE);
locationPacket.setTargetEntityId(id);
session.sendUpstreamPacket(locationPacket);
});
}
}
private void update(TrackedWaypoint waypoint) {
getWaypoint(waypoint).ifPresent(tracked -> tracked.update(waypoint.data()));
}
private void untrack(TrackedWaypoint waypoint) {
getWaypoint(waypoint).ifPresent(GeyserWaypoint::untrack);
waypoints.remove(waypointId(waypoint));
waypointColors.remove(waypoint.uuid());
}
private Optional<GeyserWaypoint> getWaypoint(TrackedWaypoint waypoint) {
return Optional.ofNullable(waypoints.get(waypointId(waypoint)));
}
private static String waypointId(TrackedWaypoint waypoint) {
return Optional.ofNullable(waypoint.uuid())
.map(UUID::toString)
.orElse(waypoint.id());
}
private void updatePlayerEntry(PlayerEntity player) {
// No need to resend the entry if the player wasn't listed anyway,
// it will become listed later with the right colour
if (!player.isListed()) {
return;
}
PlayerListPacket.Entry entry = SkinManager.buildCachedEntry(session, player);
PlayerListPacket removePacket = new PlayerListPacket();
removePacket.setAction(PlayerListPacket.Action.REMOVE);
removePacket.getEntries().add(entry);
session.sendUpstreamPacket(removePacket);
PlayerListPacket addPacket = new PlayerListPacket();
addPacket.setAction(PlayerListPacket.Action.ADD);
addPacket.getEntries().add(entry);
session.sendUpstreamPacket(addPacket);
}
public void clear() {
waypoints.clear();
session.sendGameRule("locatorBar", false);
}
}

View File

@@ -0,0 +1,42 @@
/*
* 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.session.dialog;
import net.kyori.adventure.key.Key;
import org.geysermc.geyser.util.MinecraftKey;
public final class BuiltInDialog {
public static final Key SERVER_LINKS = create("server_links");
public static final Key CUSTOM_OPTIONS = create("custom_options");
public static final Key QUICK_ACTIONS = create("quick_actions");
private BuiltInDialog() {}
private static Key create(String name) {
return MinecraftKey.key(name);
}
}

View File

@@ -0,0 +1,58 @@
/*
* 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.session.dialog;
import net.kyori.adventure.key.Key;
import org.cloudburstmc.nbt.NbtMap;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.util.MinecraftKey;
import java.util.List;
import java.util.Optional;
public class ConfirmationDialog extends DialogWithButtons {
public static final Key TYPE = MinecraftKey.key("confirmation");
private final DialogButton yes;
private final DialogButton no;
public ConfirmationDialog(GeyserSession session, NbtMap map, IdGetter idGetter) {
super(session, map, Optional.empty());
yes = DialogButton.read(session, map.get("yes"), idGetter).orElseThrow();
no = DialogButton.read(session, map.get("no"), idGetter).orElseThrow();
}
@Override
protected List<DialogButton> buttons(DialogHolder holder) {
return List.of(yes, no);
}
@Override
protected Optional<DialogButton> onCancel() {
return Optional.of(no);
}
}

View File

@@ -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.session.dialog;
import lombok.Getter;
import lombok.experimental.Accessors;
import net.kyori.adventure.key.Key;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.cloudburstmc.nbt.NbtMap;
import org.cloudburstmc.nbt.NbtType;
import org.geysermc.cumulus.form.CustomForm;
import org.geysermc.cumulus.form.Form;
import org.geysermc.cumulus.form.SimpleForm;
import org.geysermc.cumulus.form.util.FormBuilder;
import org.geysermc.cumulus.response.CustomFormResponse;
import org.geysermc.cumulus.response.FormResponse;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.registry.JavaRegistries;
import org.geysermc.geyser.session.cache.registry.RegistryEntryContext;
import org.geysermc.geyser.session.dialog.input.DialogInput;
import org.geysermc.geyser.session.dialog.input.ParsedInputs;
import org.geysermc.geyser.text.MinecraftLocale;
import org.geysermc.geyser.translator.text.MessageTranslator;
import org.geysermc.geyser.util.MinecraftKey;
import org.geysermc.mcprotocollib.protocol.data.game.Holder;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.function.ToIntFunction;
@Accessors(fluent = true)
public abstract class Dialog {
private static final Key PLAIN_MESSAGE_BODY = MinecraftKey.key("plain_message");
@Getter
private final String title;
@Getter
private final Optional<String> externalTitle;
@Getter
private final boolean canCloseWithEscape;
@Getter
private final AfterAction afterAction;
private final List<String> labels;
private final List<DialogInput<?>> inputs = new ArrayList<>();
@Getter
private final ParsedInputs defaultInputs;
protected Dialog(GeyserSession session, NbtMap map) {
title = MessageTranslator.convertFromNullableNbtTag(session, map.get("title"));
externalTitle = Optional.ofNullable(MessageTranslator.convertFromNullableNbtTag(session, map.get("external_title")));
canCloseWithEscape = map.getBoolean("can_close_with_escape", true);
afterAction = AfterAction.fromString(map.getString("after_action"));
Object bodyTag = map.get("body");
if (bodyTag == null) {
labels = List.of();
} else if (bodyTag instanceof NbtMap bodyMap) {
labels = readBody(session, bodyMap).map(List::of).orElse(List.of());
} else if (bodyTag instanceof List<?> bodyList) {
labels = new ArrayList<>();
for (Object tag : bodyList) {
if (tag instanceof NbtMap bodyMap) {
readBody(session, bodyMap).ifPresent(labels::add);
} else {
throw new IllegalStateException("Found non-NBT map in list of bodies, was: " + tag);
}
}
} else {
throw new IllegalStateException("Expected body tag to either be a NBT map or list thereof, was: " + bodyTag);
}
List<NbtMap> inputTag = map.getList("inputs", NbtType.COMPOUND);
for (NbtMap input : inputTag) {
inputs.add(DialogInput.read(session, input));
}
defaultInputs = inputs.isEmpty() ? ParsedInputs.EMPTY : new ParsedInputs(inputs);
}
private static Optional<String> readBody(GeyserSession session, NbtMap tag) {
Key type = MinecraftKey.key(tag.getString("type"));
if (type.equals(PLAIN_MESSAGE_BODY)) {
return Optional.of(MessageTranslator.convertFromNullableNbtTag(session, tag.get("contents")));
}
// Other type is item, can't display that in forms
return Optional.empty();
}
protected abstract Optional<DialogButton> onCancel();
protected FormBuilder<? extends FormBuilder<?,?,?>, ? extends Form, ? extends FormResponse> createForm(DialogHolder holder, Optional<ParsedInputs> restored) {
if (inputs.isEmpty()) {
SimpleForm.Builder builder = SimpleForm.builder()
.translator(MinecraftLocale::getLocaleString, holder.session().locale())
.title(title);
builder.content(String.join("\n\n", labels));
builder.closedOrInvalidResultHandler(() -> holder.closeDialog(onCancel()));
addCustomComponents(holder, builder);
return builder;
} else {
CustomForm.Builder builder = CustomForm.builder()
.translator(MinecraftLocale::getLocaleString, holder.session().locale())
.title(title);
for (String label : labels) {
builder.label(label);
}
restored.ifPresentOrElse(last -> last.restore(holder, builder), () -> inputs.forEach(input -> input.addComponent(builder)));
builder.closedOrInvalidResultHandler(response -> holder.closeDialog(onCancel()));
addCustomComponents(holder, builder);
return builder;
}
}
protected abstract void addCustomComponents(DialogHolder holder, CustomForm.Builder builder);
protected abstract void addCustomComponents(DialogHolder holder, SimpleForm.Builder builder);
public void sendForm(DialogHolder holder) {
holder.session().sendDialogForm(createForm(holder, Optional.empty()).build());
}
public void restoreForm(DialogHolder holder, @NonNull ParsedInputs inputs) {
holder.session().sendDialogForm(createForm(holder, Optional.of(inputs)).build());
}
protected Optional<ParsedInputs> parseInput(DialogHolder holder, CustomFormResponse response) {
ParsedInputs parsed = new ParsedInputs(inputs, response);
if (parsed.hasErrors()) {
restoreForm(holder, parsed);
return Optional.empty();
}
return Optional.of(parsed);
}
public static Dialog readDialog(RegistryEntryContext context) {
return readDialogFromNbt(context.session(), context.data(), context::getNetworkId);
}
public static Dialog readDialogFromNbt(GeyserSession session, NbtMap map, IdGetter idGetter) {
Key type = MinecraftKey.key(map.getString("type"));
if (type.equals(NoticeDialog.TYPE)) {
return new NoticeDialog(session, map, idGetter);
} else if (type.equals(ServerLinksDialog.TYPE)) {
return new ServerLinksDialog(session, map, idGetter);
} else if (type.equals(DialogListDialog.TYPE)) {
return new DialogListDialog(session, map, idGetter);
} else if (type.equals(MultiActionDialog.TYPE)) {
return new MultiActionDialog(session, map, idGetter);
} else if (type.equals(ConfirmationDialog.TYPE)) {
return new ConfirmationDialog(session, map, idGetter);
}
throw new UnsupportedOperationException("Unable to read unknown dialog type " + type + "!");
}
public static Dialog getDialogFromHolder(GeyserSession session, Holder<NbtMap> holder) {
if (holder.isId()) {
return Objects.requireNonNull(JavaRegistries.DIALOG.value(session, holder.id()));
} else {
return Dialog.readDialogFromNbt(session, holder.custom(), key -> JavaRegistries.DIALOG.networkId(session, key));
}
}
public static Dialog getDialogFromKey(GeyserSession session, Key key) {
return Objects.requireNonNull(JavaRegistries.DIALOG.value(session, key));
}
public enum AfterAction {
CLOSE,
NONE,
WAIT_FOR_RESPONSE;
public static AfterAction fromString(String string) {
for (AfterAction action : values()) {
if (action.name().toLowerCase(Locale.ROOT).equals(string)) {
return action;
}
}
return CLOSE;
}
}
@FunctionalInterface
public interface IdGetter extends ToIntFunction<Key> {}
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright (c) 2025 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.session.dialog;
import org.cloudburstmc.nbt.NbtMap;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.dialog.action.DialogAction;
import org.geysermc.geyser.translator.text.MessageTranslator;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
public record DialogButton(String label, Optional<DialogAction> action) {
public static List<DialogButton> readList(GeyserSession session, List<NbtMap> tag, Dialog.IdGetter idGetter) {
if (tag == null) {
return List.of();
}
List<DialogButton> buttons = new ArrayList<>();
for (NbtMap map : tag) {
buttons.add(read(session, map, idGetter).orElseThrow()); // Should never throw because we know map is a NbtMap
}
return buttons;
}
public static Optional<DialogButton> read(GeyserSession session, Object tag, Dialog.IdGetter idGetter) {
if (!(tag instanceof NbtMap map)) {
return Optional.empty();
}
return Optional.of(new DialogButton(MessageTranslator.convertFromNullableNbtTag(session, map.get("label")), DialogAction.read(map.get("action"), idGetter)));
}
}

View File

@@ -0,0 +1,349 @@
/*
* 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.session.dialog;
import lombok.Getter;
import lombok.experimental.Accessors;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.geysermc.cumulus.form.ModalForm;
import org.geysermc.cumulus.form.SimpleForm;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.dialog.action.DialogAction;
import org.geysermc.geyser.session.dialog.input.ParsedInputs;
import org.geysermc.geyser.text.GeyserLocale;
import org.geysermc.geyser.text.MinecraftLocale;
import org.geysermc.geyser.translator.text.MessageTranslator;
import java.util.Optional;
/**
* Used to manage an open dialog. Handles dialog input, transferring to other dialogs, and dialog "submenus" for e.g. waiting on a new dialog or confirming a command.
*
* <p>This is passed to a {@link Dialog} when using it to send a form to the client, it uses the {@link DialogHolder#runButton(Optional, ParsedInputs)} and {@link DialogHolder#closeDialog(Optional)} methods.</p>
*
* <p>
* To make it easier to understand what this class does and why it does it, here is the summed up behaviour of dialogs on Java.
* Java dialogs consist of inputs and buttons that run actions. Dialogs also have an "on cancel"/closing action, which is usually executed when the user presses ESC, or presses an "exit" button, defined by the dialog
* (note: not all dialog types can have an exit button). Dialogs can disallow closing by pressing ESC. Geyser translates clicking the "X" in the corner of a form as pressing ESC.</p>
*
* <p>
* Dialog inputs are quite simple. The user can enter what they want, and the inputs will clear once the dialog has been closed (note: <em>only</em> once the dialog has been closed. This becomes important later!).
* </p>
*
* <p>
* Dialog actions are more complicated. Dialogs can define what to do after an action has been executed (so-called "after action" behaviour). When executing an action, if the action:
* </p>
* <ul>
* <li>Opens a new dialog: the new dialog is opened, the old one is closed, its closing action not executed.</li>
* <li>Executes with "NONE" set as after action: the dialog is kept open, its current input kept. This means the dialog can only be closed by pressing ESC (when allowed), or by an exit button, if it exists.</li>
* <li>Executes with "CLOSE" as after action: the dialog is closed, its closing action not executed.</li>
* <li>Executes with "WAIT_FOR_RESPONSE" as after action: the dialog is closed, its closing action not executed. A new, temporary screen is opened telling the user Minecraft is waiting on a response from the server.<br>
* The server must then send a new dialog within 5 seconds. After this period, a "back" button will appear, allowing the user to go back into the game if no new dialog appears.
* </li>
* </ul>
*
* <p>If a new dialog is opened whilst another dialog is open, the old dialog is closed and the new dialog takes its place. The closing action of the dialog is not executed.</p>
*
* <p>
* All of this behaviour must be emulated by Geyser. That said, here are some of the things that this class must handle:
* </p>
*
* <ul>
* <li>Executing actions with after actions properly. Actions that run commands or the "WAIT_FOR_RESPONSE" after action make this especially complicated.<br>
* In the case of commands that require operator permissions or the previously mentioned after action, Geyser must open a temporary form asking the user for confirmation or telling the user to wait.
* </li>
* <li>Remember form input and restore it after returning to this dialog, e.g. by cancelling a command execution or by the "NONE" after action.</li>
* <li>
* Properly close this dialog and open other dialogs - for example, bedrock/Cumulus likes to call "close" handlers a lot, including when the client closes a currently open form
* to open a new one. As such, every time we do something, we must make sure this dialog is still considered open.
* </li>
* </ul>
*
* <p>Final note: when reading through this code, a dialog is "valid" when it is still considered open.</p>
*/
@Accessors(fluent = true)
public class DialogHolder {
@Getter
private final GeyserSession session;
private final DialogManager manager;
private final Dialog dialog;
/**
* The time at which the "wait for response" screen was sent. Used to track when to show the "back" button.
*/
private long responseWaitTime = 0;
/**
* If the "wait for response" screen is currently open and the "back" button should be shown.
*/
private boolean sendBackButton = false;
/**
* If the dialog should be closed as soon as possible (likely after a "confirm running command" screen).
*/
private boolean shouldClose = false;
private ParsedInputs lastInputs;
public DialogHolder(GeyserSession session, DialogManager manager, Dialog dialog) {
this.session = session;
this.manager = manager;
this.dialog = dialog;
}
/**
* Checks if this dialog is still valid, and if so, runs the given button (if present) with the given inputs.
* These inputs can be {@link ParsedInputs#EMPTY} when the dialog has no inputs, but can never be {@code null}. This method also runs the dialog's after action.
*/
public void runButton(Optional<DialogButton> button, @NonNull ParsedInputs inputs) {
lastInputs = inputs;
if (stillValid()) {
if (runAction(button, lastInputs)) {
runAfterAction();
}
}
}
/**
* Ticks this dialog. Ticks are only used to check when to show the "back" button on the "waiting for response" screen.
*/
public void tick() {
// Replace wait form with one with a back button if no replacement dialog was given
if (responseWaitTime > 0 && !sendBackButton && System.currentTimeMillis() - responseWaitTime > 5000) {
sendBackButton = true;
session.closeForm(); // Automatically reopens with a back button
}
}
// Called when clicking the X in the corner on a form, which we interpret as clicking escape
// Note that this method is called from the "closedOrInvalidResultHandler",
// meaning it can also be called when e.g. the bedrock client opens another form or is unable to open the form sent to it
/**
* Should be called when pressing "ESC", i.e., clicking the X in the corner of the form. This method checks if the dialog is still valid, and if so,
* closes it if the dialog allows closing by pressing ESC. If not, the dialog is reopened.
*
* <p>If the dialog was closed successfully, the given close action is also executed, if present.</p>
*/
public void closeDialog(Optional<DialogButton> onCancel) {
if (!stillValid()) {
return;
}
// Don't run close functionality if we're asking for command confirmation
if (dialog.canCloseWithEscape()) {
shouldClose = true;
if (runAction(onCancel, lastInputs == null ? dialog.defaultInputs() : lastInputs)) {
manager.close();
}
return;
}
// If player should not have been able to close the dialog, reopen it with the last inputs
reopenDialog();
}
/**
* Tries to reopen the dialog. First checks if the dialog is still valid. If it is, it checks if the dialog should close,
* and if so, closes the dialog. If not, the dialog is reopened, with its inputs restored if possible.
*/
private void reopenDialog() {
if (stillValid()) {
if (shouldClose) {
manager.close();
} else {
responseWaitTime = 0;
// lastInputs might be null here since it's possible none were sent yet
// Bedrock doesn't send them when just closing the form
if (lastInputs == null) {
dialog.sendForm(this);
} else {
dialog.restoreForm(this, lastInputs);
}
}
}
}
/**
* Runs the dialog's after action. This method assumes the dialog is still valid!
*/
private void runAfterAction() {
switch (dialog.afterAction()) {
case NONE -> {
// If no new dialog was opened, reopen this one
dialog.restoreForm(this, lastInputs);
}
case CLOSE -> {
// If no new dialog was opened, tell the manager this one is now closed
manager.close();
}
case WAIT_FOR_RESPONSE -> {
// If no new dialog was opened, open a form telling the user we're waiting on a response from the server
// This dialog is replaced with a similar form with a "back" button after 5 seconds, matching Java behaviour
responseWaitTime = System.currentTimeMillis();
sendBackButton = false;
waitForResponse();
}
}
}
/**
* Opens a "waiting for response" form. This method assumes the dialog is still valid!
*/
private void waitForResponse() {
String content;
if (sendBackButton) {
content = GeyserLocale.getPlayerLocaleString("geyser.dialogs.waiting_for_a_while", session.locale());
} else {
content = GeyserLocale.getPlayerLocaleString("geyser.dialogs.waiting_for_response", session.locale());
}
session.sendDialogForm(SimpleForm.builder()
.translator(MinecraftLocale::getLocaleString, session.locale())
.title("gui.waitingForResponse.title")
.content(content)
.optionalButton("gui.back", sendBackButton)
.closedOrInvalidResultHandler(() -> {
if (stillValid()) { // If still waiting on a new dialog
waitForResponse();
}
})
.validResultHandler(response -> manager.close()) // Back button was pressed, meaning no new dialog was sent
.build());
}
/**
* This method runs the given action, if present, with the given inputs.
*
* <p>These inputs can be {@link ParsedInputs#EMPTY} when the dialog has no inputs, but can never be {@code null}.
* The method returns {@code true} if the dialog's after action can be executed, and {@code false} if not. The latter is the case when the action opened a new
* dialog or screen, in which case the after action will not be handled or be handled by the screen, respectively.</p>
*
* <p>This method assumes the dialog is still valid!</p>
*/
private boolean runAction(Optional<DialogButton> button, @NonNull ParsedInputs inputs) {
DialogAction action = button.flatMap(DialogButton::action).orElse(null);
if (action != null) {
// Ask the user for confirmation if the dialog wants to run an unknown command or a command that requires operator permissions
if (action instanceof DialogAction.CommandAction runCommand) {
String command = runCommand.trimmedCommand(session, inputs);
String root = command.split(" ")[0];
// This check is not perfect. Ideally we'd check the entire command and see if any of its arguments require operator permissions, but, that's complicated
if (session.getRestrictedCommands().contains(root)) {
showCommandConfirmation(command, false);
return false;
} else if (!session.getKnownCommands().contains(root)) {
showCommandConfirmation(command, true);
return false;
}
session.sendCommand(command);
return true;
} else if (action instanceof DialogAction.OpenUrl openUrl) {
showUrl(openUrl.url());
return false;
} else {
action.run(session, inputs);
return !(action instanceof DialogAction.ShowDialog);
}
}
return true;
}
/**
* Opens an "are you sure you want to do this?" form. After confirmation, runs the command and the after action, or closes
* the dialog if it should be closed. When cancelled, returns back to the dialog, matching Java behaviour. This method assumes the dialog is still valid!
*/
private void showCommandConfirmation(String trimmedCommand, boolean unknown) {
Component content = Component.translatable(unknown ? "multiplayer.confirm_command.parse_errors" : "multiplayer.confirm_command.permissions_required",
Component.text(trimmedCommand).color(NamedTextColor.YELLOW));
session.sendDialogForm(ModalForm.builder()
.translator(MinecraftLocale::getLocaleString, session.locale())
.title("multiplayer.confirm_command.title")
.content(MessageTranslator.convertMessage(session, content))
.button1("gui.yes")
.button2("gui.no")
.closedOrInvalidResultHandler(() -> {
// Upon pressing "no" (or closing the form), we should return back to the dialog, even if it was supposed to close
shouldClose = false;
// Checks stillValid
reopenDialog();
})
.validResultHandler(response -> {
// stillValid check not needed here - valid result means the button was pressed, meaning no new dialog took over and closed this form
if (response.clickedFirst()) {
session.sendCommand(trimmedCommand);
if (shouldClose) {
manager.close();
} else {
runAfterAction();
}
} else {
// Pressed no, go back to dialog
shouldClose = false;
reopenDialog();
}
})
.build());
}
/**
* Opens a form to let the user know they should open a URL. Runs the after action when closed, or closes the dialog if it should be
* closed. This method assumes the dialog is still valid!
*/
private void showUrl(String url) {
String content = MessageTranslator.convertMessage(session,
Component.text(GeyserLocale.getPlayerLocaleString("geyser.dialogs.open_url", session.locale()))
.append(Component.text("\n\n"))
.append(Component.text(url))
.append(Component.text("\n\n"))
.append(Component.translatable("chat.link.warning").color(NamedTextColor.RED)));
session.sendDialogForm(SimpleForm.builder()
.translator(MinecraftLocale::getLocaleString, session.locale())
.title("chat.link.open")
.content(content)
.button("gui.ok")
.resultHandler((form, result) -> {
if (stillValid()) {
if (shouldClose) {
manager.close();
} else {
runAfterAction();
}
}
})
.build());
}
/**
* @return true if the dialog currently open is this dialog.
*/
private boolean stillValid() {
return manager.open() == this;
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright (c) 2025 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.session.dialog;
import net.kyori.adventure.key.Key;
import org.cloudburstmc.nbt.NbtMap;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.registry.JavaRegistries;
import org.geysermc.geyser.session.cache.tags.GeyserHolderSet;
import org.geysermc.geyser.session.dialog.action.DialogAction;
import org.geysermc.geyser.util.MinecraftKey;
import java.util.List;
import java.util.Optional;
public class DialogListDialog extends DialogWithButtons {
public static final Key TYPE = MinecraftKey.key("dialog_list");
private final GeyserHolderSet<Dialog> dialogs;
public DialogListDialog(GeyserSession session, NbtMap map, IdGetter idGetter) {
super(session, map, readDefaultExitAction(session, map, idGetter));
dialogs = GeyserHolderSet.readHolderSet(JavaRegistries.DIALOG, map.get("dialogs"), idGetter, dialog -> Dialog.readDialogFromNbt(session, dialog, idGetter));
}
@Override
protected List<DialogButton> buttons(DialogHolder holder) {
return dialogs.resolve(holder.session()).stream()
.map(dialog -> new DialogButton(dialog.externalTitle().orElseGet(dialog::title),
Optional.of(new DialogAction.ShowDialog(dialog))))
.toList();
}
}

View File

@@ -0,0 +1,82 @@
/*
* 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.session.dialog;
import lombok.Getter;
import lombok.experimental.Accessors;
import net.kyori.adventure.key.Key;
import org.cloudburstmc.nbt.NbtMap;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.mcprotocollib.protocol.data.game.Holder;
/**
* Small class to manage the currently open dialog.
*/
@Accessors(fluent = true)
public class DialogManager {
private final GeyserSession session;
@Getter
private DialogHolder open;
public DialogManager(GeyserSession session) {
this.session = session;
}
public void openDialog(Key dialog) {
openDialog(Dialog.getDialogFromKey(session, dialog));
}
public void openDialog(Holder<NbtMap> dialog) {
openDialog(Dialog.getDialogFromHolder(session, dialog));
}
/**
* Opens a new dialog. If a dialog was already open, this one will be closed. Its closing action will not be executed. This matches Java behaviour.
*/
public void openDialog(Dialog dialog) {
open = new DialogHolder(session, this, dialog);
session.closeForm();
dialog.sendForm(open);
}
public void tick() {
if (open != null) {
open.tick();
}
}
/**
* Closes the currently open dialog, if any. The dialog's closing action will not be executed.
*/
public void close() {
if (open != null) {
open = null;
// The form could already have been closed by now, but in the case it wasn't, close it anyway
// This won't run a closing dialog action, because the manager already regards the dialog as closed
session.closeForm();
}
}
}

View File

@@ -0,0 +1,97 @@
/*
* Copyright (c) 2025 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.session.dialog;
import org.cloudburstmc.nbt.NbtMap;
import org.geysermc.cumulus.component.DropdownComponent;
import org.geysermc.cumulus.form.CustomForm;
import org.geysermc.cumulus.form.SimpleForm;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.dialog.input.ParsedInputs;
import org.geysermc.geyser.text.GeyserLocale;
import java.util.List;
import java.util.Optional;
public abstract class DialogWithButtons extends Dialog {
protected final Optional<DialogButton> exitAction;
protected DialogWithButtons(GeyserSession session, NbtMap map, Optional<DialogButton> exitAction) {
super(session, map);
this.exitAction = exitAction;
}
protected abstract List<DialogButton> buttons(DialogHolder holder);
@Override
protected void addCustomComponents(DialogHolder holder, CustomForm.Builder builder) {
List<DialogButton> buttons = buttons(holder);
DropdownComponent.Builder dropdown = DropdownComponent.builder();
dropdown.text(GeyserLocale.getPlayerLocaleString("geyser.dialogs.select_action", holder.session().locale()));
for (DialogButton button : buttons) {
dropdown.option(button.label());
}
exitAction.ifPresent(button -> dropdown.option(button.label()));
builder.dropdown(dropdown);
builder.validResultHandler(response -> parseInput(holder, response).ifPresent(inputs -> {
int selection = response.asDropdown();
if (selection == buttons.size()) {
holder.runButton(exitAction, inputs);
} else {
holder.runButton(Optional.of(buttons.get(selection)), inputs);
}
}));
}
@Override
protected void addCustomComponents(DialogHolder holder, SimpleForm.Builder builder) {
List<DialogButton> buttons = buttons(holder);
for (DialogButton button : buttons) {
builder.button(button.label());
}
exitAction.ifPresent(button -> builder.button(button.label()));
builder.validResultHandler(response -> {
if (response.clickedButtonId() == buttons.size()) {
holder.runButton(exitAction, ParsedInputs.EMPTY);
} else {
holder.runButton(Optional.of(buttons.get(response.clickedButtonId())), ParsedInputs.EMPTY);
}
});
}
@Override
protected Optional<DialogButton> onCancel() {
return exitAction;
}
protected static Optional<DialogButton> readDefaultExitAction(GeyserSession session, NbtMap map, IdGetter idGetter) {
return DialogButton.read(session, map.get("exit_action"), idGetter);
}
}

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