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:
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -25,6 +25,6 @@
|
||||
"depends": {
|
||||
"fabricloader": ">=0.16.7",
|
||||
"fabric-api": "*",
|
||||
"minecraft": ">=1.21.5"
|
||||
"minecraft": ">=1.21.6"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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()}.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -123,7 +123,7 @@ public class SquidEntity extends AgeableWaterEntity implements Tickable {
|
||||
|
||||
@Override
|
||||
public boolean canBeLeashed() {
|
||||
return isNotLeashed();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void checkInWater() {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -66,7 +66,7 @@ public class HoglinEntity extends AnimalEntity {
|
||||
|
||||
@Override
|
||||
public boolean canBeLeashed() {
|
||||
return isNotLeashed();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +85,6 @@ public abstract class TameableEntity extends AnimalEntity {
|
||||
|
||||
@Override
|
||||
public boolean canBeLeashed() {
|
||||
return isNotLeashed();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -59,7 +59,7 @@ public class ZoglinEntity extends MonsterEntity {
|
||||
|
||||
@Override
|
||||
public boolean canBeLeashed() {
|
||||
return isNotLeashed();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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()));
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)}.
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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)));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {}
|
||||
|
||||
41
core/src/main/java/org/geysermc/geyser/session/cache/tags/DialogTag.java
vendored
Normal file
41
core/src/main/java/org/geysermc/geyser/session/cache/tags/DialogTag.java
vendored
Normal 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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
72
core/src/main/java/org/geysermc/geyser/session/cache/waypoint/AzimuthWaypoint.java
vendored
Normal file
72
core/src/main/java/org/geysermc/geyser/session/cache/waypoint/AzimuthWaypoint.java
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
50
core/src/main/java/org/geysermc/geyser/session/cache/waypoint/ChunkWaypoint.java
vendored
Normal file
50
core/src/main/java/org/geysermc/geyser/session/cache/waypoint/ChunkWaypoint.java
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
47
core/src/main/java/org/geysermc/geyser/session/cache/waypoint/CoordinatesWaypoint.java
vendored
Normal file
47
core/src/main/java/org/geysermc/geyser/session/cache/waypoint/CoordinatesWaypoint.java
vendored
Normal 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();
|
||||
}
|
||||
}
|
||||
145
core/src/main/java/org/geysermc/geyser/session/cache/waypoint/GeyserWaypoint.java
vendored
Normal file
145
core/src/main/java/org/geysermc/geyser/session/cache/waypoint/GeyserWaypoint.java
vendored
Normal 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();
|
||||
}
|
||||
}
|
||||
31
core/src/main/java/org/geysermc/geyser/session/cache/waypoint/TickingWaypoint.java
vendored
Normal file
31
core/src/main/java/org/geysermc/geyser/session/cache/waypoint/TickingWaypoint.java
vendored
Normal 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();
|
||||
}
|
||||
183
core/src/main/java/org/geysermc/geyser/session/cache/waypoint/WaypointCache.java
vendored
Normal file
183
core/src/main/java/org/geysermc/geyser/session/cache/waypoint/WaypointCache.java
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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> {}
|
||||
}
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user