3
0
Mirror von https://github.com/GeyserMC/Geyser.git synchronisiert 2024-11-19 22:40:18 +01:00

Merge branch 'master' into feature/viaproxy-platform

Dieser Commit ist enthalten in:
RK_01 2023-11-19 23:37:52 +01:00 committet von GitHub
Commit b1f055d31f
Es konnte kein GPG-Schlüssel zu dieser Signatur gefunden werden
GPG-Schlüssel-ID: 4AEE18F83AFDEB23
39 geänderte Dateien mit 606 neuen und 639 gelöschten Zeilen

28
Jenkinsfile vendored
Datei anzeigen

@ -1,28 +0,0 @@
pipeline {
agent any
tools {
gradle 'Gradle 7'
jdk 'Java 17'
}
options {
buildDiscarder(logRotator(artifactNumToKeepStr: '20'))
}
stages {
stage ('Build') {
steps {
sh 'git submodule update --init --recursive'
rtGradleRun(
usesPlugin: true,
tool: 'Gradle 7',
buildFile: 'build.gradle.kts',
tasks: 'clean build',
)
}
post {
success {
archiveArtifacts artifacts: 'bootstrap/**/build/libs/Geyser-*.jar', fingerprint: true
}
}
}
}
}

Datei anzeigen

@ -185,7 +185,7 @@ public interface CustomBlockComponents {
Builder placeAir(boolean placeAir);
Builder tags(Set<String> tags);
Builder tags(@Nullable Set<String> tags);
CustomBlockComponents build();
}

Datei anzeigen

@ -0,0 +1,61 @@
/*
* Copyright (c) 2019-2023 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.api.event.bedrock;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.geysermc.geyser.api.connection.GeyserConnection;
import org.geysermc.geyser.api.event.connection.ConnectionEvent;
/**
* Called when a Geyser session disconnects.
*/
public class SessionDisconnectEvent extends ConnectionEvent {
private String disconnectReason;
public SessionDisconnectEvent(@NonNull GeyserConnection connection, @NonNull String reason) {
super(connection);
this.disconnectReason = reason;
}
/**
* Gets the disconnect reason.
*
* @return the reason for the disconnect
*/
public @NonNull String disconnectReason() {
return disconnectReason;
}
/**
* Sets the disconnect reason, thereby overriding th original reason.
*
* @param disconnectReason the reason for the disconnect
*/
public void disconnectReason(@NonNull String disconnectReason) {
this.disconnectReason = disconnectReason;
}
}

Datei anzeigen

@ -29,6 +29,8 @@ import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.geyser.api.GeyserApi;
import java.util.Set;
/**
* This is used to store data for a custom item.
*/
@ -89,6 +91,14 @@ public interface CustomItemData {
*/
@Nullable CustomRenderOffsets renderOffsets();
/**
* Gets the item's set of tags that can be used in Molang.
* Equivalent to "tag:some_tag"
*
* @return the item's tags, if they exist
*/
@NonNull Set<String> tags();
static CustomItemData.Builder builder() {
return GeyserApi.api().provider(CustomItemData.Builder.class);
}
@ -113,6 +123,8 @@ public interface CustomItemData {
Builder renderOffsets(@Nullable CustomRenderOffsets renderOffsets);
Builder tags(@Nullable Set<String> tags);
CustomItemData build();
}
}

Datei anzeigen

@ -239,6 +239,9 @@ public interface NonVanillaCustomItemData extends CustomItemData {
@Override
Builder renderOffsets(@Nullable CustomRenderOffsets renderOffsets);
@Override
Builder tags(@Nullable Set<String> tags);
NonVanillaCustomItemData build();
}
}

Datei anzeigen

@ -32,18 +32,26 @@ import net.md_5.bungee.protocol.packet.LoginSuccess;
import net.md_5.bungee.protocol.packet.SetCompression;
public class GeyserBungeeCompressionDisabler extends ChannelOutboundHandlerAdapter {
private boolean compressionDisabled = false;
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
if (!(msg instanceof SetCompression)) {
if (msg instanceof LoginSuccess) {
// We're past the point that compression can be enabled
// Fixes https://github.com/GeyserMC/Geyser/issues/4281
// The server may send a LoginDisconnect packet after compression is set.
if (!compressionDisabled) {
if (ctx.pipeline().get("compress") != null) {
ctx.pipeline().remove("compress");
compressionDisabled = true;
}
if (ctx.pipeline().get("decompress") != null) {
ctx.pipeline().remove("decompress");
compressionDisabled = true;
}
}
if (msg instanceof LoginSuccess) {
// We're past the point that compression can be enabled
ctx.pipeline().remove(this);
}
super.write(ctx, msg, promise);

Datei anzeigen

@ -40,7 +40,6 @@ import org.geysermc.geyser.ping.IGeyserPingPassthrough;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.Arrays;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
@ -61,16 +60,11 @@ public class GeyserBungeePingPassthrough implements IGeyserPingPassthrough, List
}));
ProxyPingEvent event = future.join();
ServerPing response = event.getResponse();
GeyserPingInfo geyserPingInfo = new GeyserPingInfo(
return new GeyserPingInfo(
response.getDescriptionComponent().toLegacyText(),
new GeyserPingInfo.Players(response.getPlayers().getMax(), response.getPlayers().getOnline()),
new GeyserPingInfo.Version(response.getVersion().getName(), response.getVersion().getProtocol())
response.getPlayers().getMax(),
response.getPlayers().getOnline()
);
if (event.getResponse().getPlayers().getSample() != null) {
Arrays.stream(event.getResponse().getPlayers().getSample()).forEach(proxiedPlayer ->
geyserPingInfo.getPlayerList().add(proxiedPlayer.getName()));
}
return geyserPingInfo;
}
// This is static so pending connection can use it

Datei anzeigen

@ -144,7 +144,11 @@ public class GeyserFabricMod implements ModInitializer, GeyserBootstrap {
GeyserImpl.start();
this.geyserPingPassthrough = GeyserLegacyPingPassthrough.init(geyser);
if (geyserConfig.isLegacyPingPassthrough()) {
this.geyserPingPassthrough = GeyserLegacyPingPassthrough.init(geyser);
} else {
this.geyserPingPassthrough = new ModPingPassthrough(server, geyserLogger);
}
this.geyserCommandManager = new GeyserCommandManager(geyser);
this.geyserCommandManager.init();

Datei anzeigen

@ -0,0 +1,110 @@
/*
* Copyright (c) 2019-2023 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.platform.fabric;
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.network.Connection;
import net.minecraft.network.PacketSendListener;
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.server.MinecraftServer;
import net.minecraft.server.network.ServerStatusPacketListenerImpl;
import org.geysermc.geyser.GeyserLogger;
import org.geysermc.geyser.ping.GeyserPingInfo;
import org.geysermc.geyser.ping.IGeyserPingPassthrough;
import org.jetbrains.annotations.Nullable;
import java.net.InetSocketAddress;
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;
@Nullable
@Override
public GeyserPingInfo getPingInformation(InetSocketAddress inetSocketAddress) {
ServerStatus status = server.getStatus();
if (status == null) {
return null;
}
try {
StatusInterceptor connection = new StatusInterceptor();
ServerStatusPacketListener statusPacketListener = new ServerStatusPacketListenerImpl(status, connection);
statusPacketListener.handleStatusRequest(new ServerboundStatusRequestPacket());
// mods like MiniMOTD (that inject into the above method) have now processed the response
status = Objects.requireNonNull(connection.status, "status response");
} catch (Exception e) {
if (logger.isDebug()) {
logger.debug("Failed to listen for modified ServerStatus: " + e.getMessage());
e.printStackTrace();
}
}
String jsonDescription = net.minecraft.network.chat.Component.Serializer.toJson(status.description());
String legacyDescription = LEGACY_SERIALIZER.serialize(GSON_SERIALIZER.deserializeOr(jsonDescription, Component.empty()));
return new GeyserPingInfo(
legacyDescription,
status.players().map(ServerStatus.Players::max).orElse(1),
status.players().map(ServerStatus.Players::online).orElse(0)
);
}
/**
* Custom Connection that intercepts the status response right before it is sent
*/
private static class StatusInterceptor extends Connection {
ServerStatus status;
StatusInterceptor() {
super(PacketFlow.SERVERBOUND); // we are the server.
}
@Override
public void send(Packet<?> packet, @Nullable PacketSendListener packetSendListener, boolean bl) {
if (packet instanceof ClientboundStatusResponsePacket statusResponse) {
status = statusResponse.status();
}
super.send(packet, packetSendListener, bl);
}
}
}

Datei anzeigen

@ -27,7 +27,6 @@ package org.geysermc.geyser.platform.spigot;
import com.destroystokyo.paper.event.server.PaperServerListPingEvent;
import com.destroystokyo.paper.network.StatusClient;
import com.destroystokyo.paper.profile.PlayerProfile;
import org.bukkit.Bukkit;
import org.geysermc.geyser.network.GameProtocol;
import org.geysermc.geyser.ping.GeyserPingInfo;
@ -81,16 +80,7 @@ public final class GeyserPaperPingPassthrough implements IGeyserPingPassthrough
players = new GeyserPingInfo.Players(event.getMaxPlayers(), event.getNumPlayers());
}
GeyserPingInfo geyserPingInfo = new GeyserPingInfo(event.getMotd(), players,
new GeyserPingInfo.Version(Bukkit.getVersion(), GameProtocol.getJavaProtocolVersion()));
if (!event.shouldHidePlayers()) {
for (PlayerProfile profile : event.getPlayerSample()) {
geyserPingInfo.getPlayerList().add(profile.getName());
}
}
return geyserPingInfo;
return new GeyserPingInfo(event.getMotd(), players);
} catch (Exception | LinkageError e) { // LinkageError in the event that method/constructor signatures change
logger.debug("Error while getting Paper ping passthrough: " + e);
return null;

Datei anzeigen

@ -30,7 +30,6 @@ import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.server.ServerListPingEvent;
import org.bukkit.util.CachedServerIcon;
import org.geysermc.geyser.network.GameProtocol;
import org.geysermc.geyser.ping.GeyserPingInfo;
import org.geysermc.geyser.ping.IGeyserPingPassthrough;
@ -50,12 +49,7 @@ public class GeyserSpigotPingPassthrough implements IGeyserPingPassthrough {
try {
ServerListPingEvent event = new GeyserPingEvent(inetSocketAddress.getAddress(), Bukkit.getMotd(), Bukkit.getOnlinePlayers().size(), Bukkit.getMaxPlayers());
Bukkit.getPluginManager().callEvent(event);
GeyserPingInfo geyserPingInfo = new GeyserPingInfo(event.getMotd(),
new GeyserPingInfo.Players(event.getMaxPlayers(), event.getNumPlayers()),
new GeyserPingInfo.Version(Bukkit.getVersion(), GameProtocol.getJavaProtocolVersion()) // thanks Spigot for not exposing this, just default to latest
);
Bukkit.getOnlinePlayers().stream().map(Player::getName).forEach(geyserPingInfo.getPlayerList()::add);
return geyserPingInfo;
return new GeyserPingInfo(event.getMotd(), event.getMaxPlayers(), event.getNumPlayers());
} catch (Exception | LinkageError e) { // LinkageError in the event that method/constructor signatures change
logger.debug("Error while getting Bukkit ping passthrough: " + e);
return null;

Datei anzeigen

@ -54,19 +54,11 @@ public class GeyserVelocityPingPassthrough implements IGeyserPingPassthrough {
} catch (ExecutionException | InterruptedException e) {
throw new RuntimeException(e);
}
GeyserPingInfo geyserPingInfo = new GeyserPingInfo(
return new GeyserPingInfo(
LegacyComponentSerializer.legacy('§').serialize(event.getPing().getDescriptionComponent()),
new GeyserPingInfo.Players(
event.getPing().getPlayers().orElseThrow(IllegalStateException::new).getMax(),
event.getPing().getPlayers().orElseThrow(IllegalStateException::new).getOnline()
),
new GeyserPingInfo.Version(
event.getPing().getVersion().getName(),
event.getPing().getVersion().getProtocol()
)
event.getPing().getPlayers().map(ServerPing.Players::getMax).orElse(1),
event.getPing().getPlayers().map(ServerPing.Players::getOnline).orElse(0)
);
event.getPing().getPlayers().get().getSample().stream().map(ServerPing.SamplePlayer::getName).forEach(geyserPingInfo.getPlayerList()::add);
return geyserPingInfo;
}
private static class GeyserInboundConnection implements InboundConnection {

Datei anzeigen

@ -206,9 +206,9 @@ public class GeyserVelocityPlugin implements GeyserBootstrap {
// After this bound, we know that the channel initializer cannot change without it being ineffective for Velocity, too
geyserInjector.initializeLocalChannel(this);
}
}
INITIALIZED = true;
INITIALIZED = true;
}
}
@Override

Datei anzeigen

@ -25,6 +25,8 @@
package org.geysermc.geyser;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.geyser.command.GeyserCommandManager;
import org.geysermc.geyser.configuration.GeyserConfiguration;
import org.geysermc.geyser.dump.BootstrapDumpInfo;
@ -32,8 +34,6 @@ import org.geysermc.geyser.level.GeyserWorldManager;
import org.geysermc.geyser.level.WorldManager;
import org.geysermc.geyser.ping.IGeyserPingPassthrough;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.InputStream;
import java.net.SocketAddress;
import java.nio.file.Path;
@ -79,6 +79,7 @@ public interface GeyserBootstrap {
*
* @return The current PingPassthrough manager
*/
@Nullable
IGeyserPingPassthrough getGeyserPingPassthrough();
/**
@ -151,7 +152,7 @@ public interface GeyserBootstrap {
* @param resource Resource to get
* @return InputStream of the given resource
*/
default @Nonnull InputStream getResource(String resource) {
default @NonNull InputStream getResource(String resource) {
InputStream stream = getResourceOrNull(resource);
if (stream == null) {
throw new AssertionError("Unable to find resource: " + resource);
@ -162,7 +163,7 @@ public interface GeyserBootstrap {
/**
* @return the bind address being used by the Java server.
*/
@Nonnull
@NonNull
String getServerBindAddress();
/**

Datei anzeigen

@ -32,6 +32,8 @@ import org.geysermc.geyser.command.GeyserCommandSource;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.text.GeyserLocale;
import java.util.concurrent.TimeUnit;
public class ReloadCommand extends GeyserCommand {
private final GeyserImpl geyser;
@ -52,7 +54,8 @@ public class ReloadCommand extends GeyserCommand {
sender.sendMessage(message);
geyser.getSessionManager().disconnectAll("geyser.commands.reload.kick");
geyser.reload();
//FIXME Without the tiny wait, players do not get kicked - same happens when Geyser tries to disconnect all sessions on shutdown
geyser.getScheduledThread().schedule(geyser::reload, 10, TimeUnit.MILLISECONDS);
}
@Override

Datei anzeigen

@ -57,9 +57,6 @@ public interface GeyserConfiguration {
@JsonIgnore
boolean isPassthroughMotd();
@JsonIgnore
boolean isPassthroughProtocolName();
@JsonIgnore
boolean isPassthroughPlayerCounts();

Datei anzeigen

@ -75,9 +75,6 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration
@JsonProperty("passthrough-player-counts")
private boolean isPassthroughPlayerCounts = false;
@JsonProperty("passthrough-protocol-name")
private boolean isPassthroughProtocolName = false;
@JsonProperty("legacy-ping-passthrough")
private boolean isLegacyPingPassthrough = false;

Datei anzeigen

@ -26,6 +26,7 @@
package org.geysermc.geyser.entity.type;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata;
import net.kyori.adventure.text.Component;
import org.cloudburstmc.math.vector.Vector3f;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes;
@ -49,6 +50,12 @@ public class TextDisplayEntity extends Entity {
this.dirtyMetadata.put(EntityDataTypes.NAMETAG_ALWAYS_SHOW, (byte) 1);
}
@Override
public void setDisplayNameVisible(BooleanEntityMetadata entityMetadata) {
// Don't allow the display name to be hidden - messes with our armor stand.
// On JE: Hiding the display name still shows the display entity text.
}
public void setText(EntityMetadata<Component, ?> entityMetadata) {
this.dirtyMetadata.put(EntityDataTypes.NAME, MessageTranslator.convertMessage(entityMetadata.getValue()));
}

Datei anzeigen

@ -254,4 +254,17 @@ public class SessionPlayerEntity extends PlayerEntity {
public UUID getTabListUuid() {
return session.getAuthData().uuid();
}
public void resetMetadata() {
// Reset all metadata to their default values
// This is used when a player respawns
this.initializeMetadata();
// Reset air
this.resetAir();
}
public void resetAir() {
this.setAirSupply(getMaxAir());
}
}

Datei anzeigen

@ -28,10 +28,14 @@ package org.geysermc.geyser.item;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.geyser.api.item.custom.CustomItemData;
import org.geysermc.geyser.api.item.custom.CustomItemOptions;
import org.geysermc.geyser.api.item.custom.CustomRenderOffsets;
import org.jetbrains.annotations.NotNull;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
@EqualsAndHashCode
@ToString
@ -44,6 +48,7 @@ public class GeyserCustomItemData implements CustomItemData {
private final boolean displayHandheld;
private final int textureSize;
private final CustomRenderOffsets renderOffsets;
private final Set<String> tags;
public GeyserCustomItemData(String name,
CustomItemOptions customItemOptions,
@ -52,7 +57,8 @@ public class GeyserCustomItemData implements CustomItemData {
boolean allowOffhand,
boolean displayHandheld,
int textureSize,
CustomRenderOffsets renderOffsets) {
CustomRenderOffsets renderOffsets,
Set<String> tags) {
this.name = name;
this.customItemOptions = customItemOptions;
this.displayName = displayName;
@ -61,10 +67,11 @@ public class GeyserCustomItemData implements CustomItemData {
this.displayHandheld = displayHandheld;
this.textureSize = textureSize;
this.renderOffsets = renderOffsets;
this.tags = tags;
}
@Override
public @NotNull String name() {
public @NonNull String name() {
return name;
}
@ -74,12 +81,12 @@ public class GeyserCustomItemData implements CustomItemData {
}
@Override
public @NotNull String displayName() {
public @NonNull String displayName() {
return displayName;
}
@Override
public @NotNull String icon() {
public @NonNull String icon() {
return icon;
}
@ -103,6 +110,11 @@ public class GeyserCustomItemData implements CustomItemData {
return renderOffsets;
}
@Override
public @NonNull Set<String> tags() {
return tags;
}
public static class CustomItemDataBuilder implements Builder {
protected String name = null;
protected CustomItemOptions customItemOptions = null;
@ -113,6 +125,7 @@ public class GeyserCustomItemData implements CustomItemData {
protected boolean displayHandheld = false;
protected int textureSize = 16;
protected CustomRenderOffsets renderOffsets = null;
protected Set<String> tags = new HashSet<>();
@Override
public Builder name(@NonNull String name) {
@ -162,6 +175,12 @@ public class GeyserCustomItemData implements CustomItemData {
return this;
}
@Override
public Builder tags(@Nullable Set<String> tags) {
this.tags = Objects.requireNonNullElseGet(tags, Set::of);
return this;
}
@Override
public CustomItemData build() {
if (this.name == null || this.customItemOptions == null) {
@ -174,7 +193,7 @@ public class GeyserCustomItemData implements CustomItemData {
if (this.icon == null) {
this.icon = this.name;
}
return new GeyserCustomItemData(this.name, this.customItemOptions, this.displayName, this.icon, this.allowOffhand, this.displayHandheld, this.textureSize, this.renderOffsets);
return new GeyserCustomItemData(this.name, this.customItemOptions, this.displayName, this.icon, this.allowOffhand, this.displayHandheld, this.textureSize, this.renderOffsets, this.tags);
}
}
}

Datei anzeigen

@ -61,7 +61,7 @@ public final class GeyserNonVanillaCustomItemData extends GeyserCustomItemData i
public GeyserNonVanillaCustomItemData(NonVanillaCustomItemDataBuilder builder) {
super(builder.name, builder.customItemOptions, builder.displayName, builder.icon, builder.allowOffhand,
builder.displayHandheld, builder.textureSize, builder.renderOffsets);
builder.displayHandheld, builder.textureSize, builder.renderOffsets, builder.tags);
this.identifier = builder.identifier;
this.javaId = builder.javaId;
@ -237,6 +237,11 @@ public final class GeyserNonVanillaCustomItemData extends GeyserCustomItemData i
return (NonVanillaCustomItemData.Builder) super.renderOffsets(renderOffsets);
}
@Override
public NonVanillaCustomItemData.Builder tags(@Nullable Set<String> tags) {
return (NonVanillaCustomItemData.Builder) super.tags(tags);
}
@Override
public NonVanillaCustomItemData.Builder identifier(@NonNull String identifier) {
this.identifier = identifier;

Datei anzeigen

@ -1,174 +0,0 @@
/*
* Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.item.components;
import org.cloudburstmc.nbt.NbtMap;
import org.cloudburstmc.nbt.NbtType;
import java.util.ArrayList;
import java.util.List;
public class ToolBreakSpeedsUtils {
public static int toolTierToSpeed(String toolTier) {
ToolTier tier = ToolTier.getByName(toolTier);
if (tier != null) {
return tier.getSpeed();
}
return 0;
}
private static NbtMap createTagBreakSpeed(int speed, String... tags) {
StringBuilder builder = new StringBuilder("query.any_tag('");
builder.append(tags[0]);
for (int i = 1; i < tags.length; i++) {
builder.append("', '").append(tags[i]);
}
builder.append("')");
return NbtMap.builder()
.putCompound("block", NbtMap.builder()
.putString("tags", builder.toString())
.build())
.putCompound("on_dig", NbtMap.builder()
.putCompound("condition", NbtMap.builder()
.putString("expression", "")
.putInt("version", -1)
.build())
.putString("event", "tool_durability")
.putString("target", "self")
.build())
.putInt("speed", speed)
.build();
}
private static NbtMap createBreakSpeed(int speed, String block) {
return NbtMap.builder()
.putCompound("block", NbtMap.builder()
.putString("name", block).build())
.putCompound("on_dig", NbtMap.builder()
.putCompound("condition", NbtMap.builder()
.putString("expression", "")
.putInt("version", -1)
.build())
.putString("event", "tool_durability")
.putString("target", "self")
.build())
.putInt("speed", speed)
.build();
}
private static NbtMap createDigger(List<NbtMap> speeds) {
return NbtMap.builder()
.putList("destroy_speeds", NbtType.COMPOUND, speeds)
.putCompound("on_dig", NbtMap.builder()
.putCompound("condition", NbtMap.builder()
.putString("expression", "")
.putInt("version", -1)
.build())
.putString("event", "tool_durability")
.putString("target", "self")
.build())
.putBoolean("use_efficiency", true)
.build();
}
public static NbtMap getAxeDigger(int speed) {
List<NbtMap> speeds = new ArrayList<>();
speeds.add(createTagBreakSpeed(speed, "wood", "pumpkin", "plant"));
return createDigger(speeds);
}
public static NbtMap getPickaxeDigger(int speed, String toolTier) {
List<NbtMap> speeds = new ArrayList<>();
if (toolTier.equals(ToolTier.DIAMOND.toString()) || toolTier.equals(ToolTier.NETHERITE.toString())) {
speeds.add(createTagBreakSpeed(speed, "iron_pick_diggable", "diamond_pick_diggable"));
} else {
speeds.add(createTagBreakSpeed(speed, "iron_pick_diggable"));
}
speeds.add(createTagBreakSpeed(speed, "stone", "metal", "rail", "mob_spawner"));
return createDigger(speeds);
}
public static NbtMap getShovelDigger(int speed) {
List<NbtMap> speeds = new ArrayList<>();
speeds.add(createTagBreakSpeed(speed, "dirt", "sand", "gravel", "grass", "snow"));
return createDigger(speeds);
}
public static NbtMap getSwordDigger(int speed) {
List<NbtMap> speeds = new ArrayList<>();
speeds.add(createBreakSpeed(speed, "minecraft:web"));
speeds.add(createBreakSpeed(speed, "minecraft:bamboo"));
return createDigger(speeds);
}
public static NbtMap getHoeDigger(int speed) {
List<NbtMap> speeds = new ArrayList<>();
speeds.add(createBreakSpeed(speed, "minecraft:leaves"));
speeds.add(createBreakSpeed(speed, "minecraft:leaves2"));
speeds.add(createBreakSpeed(speed, "minecraft:azalea_leaves"));
speeds.add(createBreakSpeed(speed, "minecraft:azalea_leaves_flowered"));
speeds.add(createBreakSpeed(speed, "minecraft:sculk"));
speeds.add(createBreakSpeed(speed, "minecraft:sculk_catalyst"));
speeds.add(createBreakSpeed(speed, "minecraft:sculk_sensor"));
speeds.add(createBreakSpeed(speed, "minecraft:sculk_shrieker"));
speeds.add(createBreakSpeed(speed, "minecraft:sculk_vein"));
speeds.add(createBreakSpeed(speed, "minecraft:nether_wart_block"));
speeds.add(createBreakSpeed(speed, "minecraft:warped_wart_block"));
speeds.add(createBreakSpeed(speed, "minecraft:hay_block"));
speeds.add(createBreakSpeed(speed, "minecraft:moss_block"));
speeds.add(createBreakSpeed(speed, "minecraft:shroomlight"));
speeds.add(createBreakSpeed(speed, "minecraft:sponge"));
speeds.add(createBreakSpeed(speed, "minecraft:target"));
return createDigger(speeds);
}
public static NbtMap getShearsDigger(int speed) {
List<NbtMap> speeds = new ArrayList<>();
speeds.add(createBreakSpeed(speed, "minecraft:web"));
speeds.add(createBreakSpeed(speed, "minecraft:leaves"));
speeds.add(createBreakSpeed(speed, "minecraft:leaves2"));
speeds.add(createBreakSpeed(speed, "minecraft:azalea_leaves"));
speeds.add(createBreakSpeed(speed, "minecraft:azalea_leaves_flowered"));
speeds.add(createBreakSpeed(speed, "minecraft:wool"));
speeds.add(createBreakSpeed(speed, "minecraft:glow_lichen"));
speeds.add(createBreakSpeed(speed, "minecraft:vine"));
return createDigger(speeds);
}
}

Datei anzeigen

@ -62,6 +62,7 @@ public final class BlockStateValues {
private static final IntSet ALL_PISTON_HEADS = new IntOpenHashSet();
private static final IntSet MOVING_PISTONS = new IntOpenHashSet();
private static final Int2ByteMap SKULL_VARIANTS = new FixedInt2ByteMap();
private static final IntSet SKULL_POWERED = new IntOpenHashSet();
private static final Int2ByteMap SKULL_ROTATIONS = new Int2ByteOpenHashMap();
private static final Int2IntMap SKULL_WALL_DIRECTIONS = new Int2IntOpenHashMap();
private static final Int2ByteMap SHULKERBOX_DIRECTIONS = new FixedInt2ByteMap();
@ -172,6 +173,13 @@ public final class BlockStateValues {
SKULL_ROTATIONS.put(javaBlockState, (byte) skullRotation.intValue());
}
if (javaId.startsWith("minecraft:dragon_head[") || javaId.startsWith("minecraft:piglin_head[")
|| javaId.startsWith("minecraft:dragon_wall_head[") || javaId.startsWith("minecraft:piglin_wall_head[")) {
if (javaId.contains("powered=true")) {
SKULL_POWERED.add(javaBlockState);
}
}
if (javaId.contains("wall_skull") || javaId.contains("wall_head")) {
String direction = javaId.substring(javaId.lastIndexOf("facing=") + 7, javaId.lastIndexOf("powered=") - 1);
int rotation = switch (direction) {
@ -448,6 +456,17 @@ public final class BlockStateValues {
return SKULL_ROTATIONS.getOrDefault(state, (byte) -1);
}
/**
* As of Java 1.20.2:
* Skull powered states are part of the namespaced ID in Java Edition, but part of the block entity tag in Bedrock.
*
* @param state BlockState of the block
* @return true if this skull is currently being powered.
*/
public static boolean isSkullPowered(int state) {
return SKULL_POWERED.contains(state);
}
/**
* Skull rotations are part of the namespaced ID in Java Edition, but part of the block entity tag in Bedrock.
* This gives a integer rotation that Bedrock can use.

Datei anzeigen

@ -31,18 +31,19 @@ import it.unimi.dsi.fastutil.objects.Object2ObjectMaps;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import lombok.Value;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.geyser.api.block.custom.component.BoxComponent;
import org.geysermc.geyser.api.block.custom.component.CustomBlockComponents;
import org.geysermc.geyser.api.block.custom.component.GeometryComponent;
import org.geysermc.geyser.api.block.custom.component.MaterialInstance;
import org.geysermc.geyser.api.block.custom.component.PlacementConditions;
import org.geysermc.geyser.api.block.custom.component.TransformationComponent;
import org.jetbrains.annotations.NotNull;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Objects;
@Value
public class GeyserCustomBlockComponents implements CustomBlockComponents {
@ -152,7 +153,7 @@ public class GeyserCustomBlockComponents implements CustomBlockComponents {
}
@Override
public @NotNull Set<String> tags() {
public @NonNull Set<String> tags() {
return tags;
}
@ -170,7 +171,7 @@ public class GeyserCustomBlockComponents implements CustomBlockComponents {
protected TransformationComponent transformation;
protected boolean unitCube = false;
protected boolean placeAir = false;
protected final Set<String> tags = new HashSet<>();
protected Set<String> tags = new HashSet<>();
private void validateBox(BoxComponent box) {
if (box == null) {
@ -217,7 +218,7 @@ public class GeyserCustomBlockComponents implements CustomBlockComponents {
}
@Override
public Builder materialInstance(@NotNull String name, @NotNull MaterialInstance materialInstance) {
public Builder materialInstance(@NonNull String name, @NonNull MaterialInstance materialInstance) {
this.materialInstances.put(name, materialInstance);
return this;
}
@ -292,8 +293,8 @@ public class GeyserCustomBlockComponents implements CustomBlockComponents {
}
@Override
public Builder tags(Set<String> tags) {
this.tags.addAll(tags);
public Builder tags(@Nullable Set<String> tags) {
this.tags = Objects.requireNonNullElseGet(tags, Set::of);
return this;
}

Datei anzeigen

@ -93,6 +93,14 @@ public final class GameProtocol {
return session.getUpstream().getProtocolVersion() < Bedrock_v594.CODEC.getProtocolVersion();
}
/**
* @param session the session to check
* @return true if the session needs an experiment for recipe unlocking
*/
public static boolean isUsingExperimentalRecipeUnlocking(GeyserSession session) {
return session.getUpstream().getProtocolVersion() == Bedrock_v594.CODEC.getProtocolVersion();
}
/**
* Gets the {@link PacketCodec} for Minecraft: Java Edition.
*

Datei anzeigen

@ -1,303 +0,0 @@
/*
* Copyright (c) 2019-2022 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.network;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.ping.GeyserPingInfo;
import org.geysermc.geyser.translator.text.MessageTranslator;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ThreadLocalRandom;
public class QueryPacketHandler {
public static final byte HANDSHAKE = 0x09;
public static final byte STATISTICS = 0x00;
private final GeyserImpl geyser;
private final InetSocketAddress sender;
private final byte type;
private final int sessionId;
private byte[] token;
/**
* The Query packet handler instance. The unsigned short magic handshake should already be read at this point,
* and the packet should be verified to have enough buffer space to be a qualified query packet.
*
* @param geyser Geyser
* @param sender The Sender IP/Port for the Query
* @param buffer The Query data
*/
public QueryPacketHandler(GeyserImpl geyser, InetSocketAddress sender, ByteBuf buffer) {
this.geyser = geyser;
this.sender = sender;
this.type = buffer.readByte();
this.sessionId = buffer.readInt();
regenerateToken();
handle();
}
/**
* Checks the packet is in fact a query packet
*
* @param buffer Query data
* @return if the packet is a query packet
*/
public static boolean isQueryPacket(ByteBuf buffer) {
// 2 for magic short, 1 for type byte and 4 for session ID int
return buffer.readableBytes() >= (2 + 1 + 4) && buffer.readUnsignedShort() == 0xFEFD;
}
/**
* Handles the query
*/
private void handle() {
switch (type) {
case HANDSHAKE:
sendToken();
break;
case STATISTICS:
sendQueryData();
break;
}
}
/**
* Sends the token to the sender
*/
private void sendToken() {
ByteBuf reply = ByteBufAllocator.DEFAULT.ioBuffer(10);
reply.writeByte(HANDSHAKE);
reply.writeInt(sessionId);
reply.writeBytes(getTokenString(this.token, this.sender.getAddress()));
reply.writeByte(0);
sendPacket(reply);
}
/**
* Sends the query data to the sender
*/
private void sendQueryData() {
byte[] gameData = getGameData();
byte[] playerData = getPlayers();
ByteBuf reply = ByteBufAllocator.DEFAULT.ioBuffer(1 + 4 + gameData.length + playerData.length);
reply.writeByte(STATISTICS);
reply.writeInt(sessionId);
// Game Info
reply.writeBytes(gameData);
// Players
reply.writeBytes(playerData);
sendPacket(reply);
}
/**
* Gets the game data for the query
*
* @return the game data for the query
*/
private byte[] getGameData() {
ByteArrayOutputStream query = new ByteArrayOutputStream();
GeyserPingInfo pingInfo = null;
String motd;
String currentPlayerCount;
String maxPlayerCount;
String map;
if (geyser.getConfig().isPassthroughMotd() || geyser.getConfig().isPassthroughPlayerCounts()) {
pingInfo = geyser.getBootstrap().getGeyserPingPassthrough().getPingInformation();
}
if (geyser.getConfig().isPassthroughMotd() && pingInfo != null) {
String[] javaMotd = MessageTranslator.convertMessageLenient(pingInfo.getDescription()).split("\n");
motd = javaMotd[0].trim(); // First line of the motd.
} else {
motd = geyser.getConfig().getBedrock().primaryMotd();
}
// If passthrough player counts is enabled lets get players from the server
if (geyser.getConfig().isPassthroughPlayerCounts() && pingInfo != null) {
currentPlayerCount = String.valueOf(pingInfo.getPlayers().getOnline());
maxPlayerCount = String.valueOf(pingInfo.getPlayers().getMax());
} else {
currentPlayerCount = String.valueOf(geyser.getSessionManager().getSessions().size());
maxPlayerCount = String.valueOf(geyser.getConfig().getMaxPlayers());
}
// If passthrough protocol name is enabled let's get the protocol name from the ping response.
if (geyser.getConfig().isPassthroughProtocolName() && pingInfo != null) {
map = pingInfo.getVersion().getName();
} else {
map = GeyserImpl.NAME;
}
// Create a hashmap of all game data needed in the query
Map<String, String> gameData = new HashMap<>();
gameData.put("hostname", motd);
gameData.put("gametype", "SMP");
gameData.put("game_id", "MINECRAFT");
gameData.put("version", GeyserImpl.NAME + " (" + GeyserImpl.GIT_VERSION + ") " + GameProtocol.DEFAULT_BEDROCK_CODEC.getMinecraftVersion());
gameData.put("plugins", "");
gameData.put("map", map);
gameData.put("numplayers", currentPlayerCount);
gameData.put("maxplayers", maxPlayerCount);
gameData.put("hostport", String.valueOf(geyser.getConfig().getBedrock().port()));
gameData.put("hostip", geyser.getConfig().getBedrock().address());
try {
writeString(query, "GeyserMC");
query.write((byte) 0x80);
query.write((byte) 0x00);
// Fills the game data
for (Map.Entry<String, String> entry : gameData.entrySet()) {
writeString(query, entry.getKey());
writeString(query, entry.getValue());
}
// Final byte to show the end of the game data
query.write(new byte[] { 0x00, 0x01 });
return query.toByteArray();
} catch (IOException e) {
e.printStackTrace();
return new byte[0];
}
}
/**
* Generate a byte[] storing the player names
*
* @return The byte[] representation of players
*/
private byte[] getPlayers() {
ByteArrayOutputStream query = new ByteArrayOutputStream();
GeyserPingInfo pingInfo = null;
if (geyser.getConfig().isPassthroughMotd() || geyser.getConfig().isPassthroughPlayerCounts()) {
pingInfo = geyser.getBootstrap().getGeyserPingPassthrough().getPingInformation();
}
try {
// Start the player section
writeString(query, "player_");
query.write((byte) 0x00);
// Fill player names
if (pingInfo != null) {
for (String username : pingInfo.getPlayerList()) {
writeString(query, username);
}
}
// Final byte to show the end of the player data
query.write((byte) 0x00);
return query.toByteArray();
} catch (IOException e) {
e.printStackTrace();
return new byte[0];
}
}
/**
* Partially mimics {@link java.io.DataOutputStream#writeBytes(String)} which is what the Minecraft server uses as of 1.17.1.
*/
private void writeString(OutputStream stream, String value) throws IOException {
int length = value.length();
for (int i = 0; i < length; i++) {
stream.write((byte) value.charAt(i));
}
// Padding to indicate the end of the string
stream.write((byte) 0x00);
}
/**
* Sends a packet to the sender
*
* @param data packet data
*/
private void sendPacket(ByteBuf data) {
// geyser.getBedrockServer().getRakNet().send(sender, data);
}
/**
* Regenerates a token
*/
public void regenerateToken() {
byte[] token = new byte[16];
for (int i = 0; i < 16; i++) {
token[i] = (byte) ThreadLocalRandom.current().nextInt(255);
}
this.token = token;
}
/**
* Gets an MD5 token for the current IP/Port.
* This should reset every 30 seconds but a new one is generated per instance
* Seems wasteful to code something in to clear it when it has no use.
*
* @param token the token
* @param address the address
* @return an MD5 token for the current IP/Port
*/
public static byte[] getTokenString(byte[] token, InetAddress address) {
try {
// Generate an MD5 hash from the address
MessageDigest digest = MessageDigest.getInstance("MD5");
digest.update(address.toString().getBytes(StandardCharsets.UTF_8));
digest.update(token);
// Get the first 4 bytes of the digest
byte[] digestBytes = Arrays.copyOf(digest.digest(), 4);
// Convert the bytes to a buffer
ByteBuffer byteBuffer = ByteBuffer.wrap(digestBytes);
// Turn the number into a null terminated string
return (byteBuffer.getInt() + "\0").getBytes();
} catch (NoSuchAlgorithmException e) {
return (ByteBuffer.allocate(4).putInt(ThreadLocalRandom.current().nextInt()).getInt() + "\0").getBytes();
}
}
}

Datei anzeigen

@ -69,6 +69,7 @@ import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.IntFunction;
import java.util.function.Supplier;
public final class GeyserServer {
private static final boolean PRINT_DEBUG_PINGS = Boolean.parseBoolean(System.getProperty("Geyser.PrintPingsInDebugMode", "true"));
@ -163,8 +164,9 @@ public final class GeyserServer {
if (System.getProperties().contains("disableNativeEventLoop")) {
this.geyser.getLogger().debug("EventLoop type is NIO because native event loops are disabled.");
} else {
this.geyser.getLogger().debug("Reason for no Epoll: " + Epoll.unavailabilityCause().toString());
this.geyser.getLogger().debug("Reason for no KQueue: " + KQueue.unavailabilityCause().toString());
// Use lambda here, not method reference, or else NoClassDefFoundError for Epoll/KQueue will not be caught
this.geyser.getLogger().debug("Reason for no Epoll: " + throwableOrCaught(() -> Epoll.unavailabilityCause()));
this.geyser.getLogger().debug("Reason for no KQueue: " + throwableOrCaught(() -> KQueue.unavailabilityCause()));
}
}
}
@ -230,7 +232,9 @@ public final class GeyserServer {
GeyserPingInfo pingInfo = null;
if (config.isPassthroughMotd() || config.isPassthroughPlayerCounts()) {
IGeyserPingPassthrough pingPassthrough = geyser.getBootstrap().getGeyserPingPassthrough();
pingInfo = pingPassthrough.getPingInformation(inetSocketAddress);
if (pingPassthrough != null) {
pingInfo = pingPassthrough.getPingInformation(inetSocketAddress);
}
}
BedrockPong pong = new BedrockPong()
@ -245,8 +249,8 @@ public final class GeyserServer {
if (config.isPassthroughMotd() && pingInfo != null && pingInfo.getDescription() != null) {
String[] motd = MessageTranslator.convertMessageLenient(pingInfo.getDescription()).split("\n");
String mainMotd = motd[0]; // First line of the motd.
String subMotd = (motd.length != 1) ? motd[1] : GeyserImpl.NAME; // Second line of the motd if present, otherwise default.
String mainMotd = (motd.length > 0) ? motd[0] : config.getBedrock().primaryMotd(); // First line of the motd.
String subMotd = (motd.length > 1) ? motd[1] : config.getBedrock().secondaryMotd(); // Second line of the motd if present, otherwise default.
pong.motd(mainMotd.trim());
pong.subMotd(subMotd.trim()); // Trimmed to shift it to the left, prevents the universe from collapsing on us just because we went 2 characters over the text box's limit.
@ -312,6 +316,17 @@ public final class GeyserServer {
return pong;
}
/**
* @return the throwable from the given supplier, or the throwable caught while calling the supplier.
*/
private static Throwable throwableOrCaught(Supplier<Throwable> supplier) {
try {
return supplier.get();
} catch (Throwable throwable) {
return throwable;
}
}
private static Transport compatibleTransport() {
TransportHelper.TransportMethod transportMethod = TransportHelper.determineTransportMethod();
if (transportMethod == TransportHelper.TransportMethod.EPOLL) {

Datei anzeigen

@ -25,34 +25,37 @@
package org.geysermc.geyser.ping;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSetter;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Data;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.util.ArrayList;
import java.util.Collection;
/**
* The structure of this class and its nested classes are specifically
* designed for the format received by {@link GeyserLegacyPingPassthrough}.
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class GeyserPingInfo {
@Nullable
private String description;
private Players players;
private Version version;
@JsonIgnore
private Collection<String> playerList = new ArrayList<>();
public GeyserPingInfo() {
// for json mapping
}
public GeyserPingInfo(String description, Players players, Version version) {
public GeyserPingInfo(@Nullable String description, Players players) {
this.description = description;
this.players = players;
this.version = version;
}
public GeyserPingInfo(@Nullable String description, int maxPlayers, int onlinePlayers) {
this.description = description;
this.players = new Players(maxPlayers, onlinePlayers);
}
@JsonSetter("description")
@ -68,6 +71,7 @@ public class GeyserPingInfo {
private int online;
public Players() {
// for json mapping
}
public Players(int max, int online) {
@ -75,19 +79,4 @@ public class GeyserPingInfo {
this.online = online;
}
}
@Data
public static class Version {
private String name;
private int protocol;
public Version() {
}
public Version(String name, int protocol) {
this.name = name;
this.protocol = protocol;
}
}
}

Datei anzeigen

@ -26,7 +26,6 @@
package org.geysermc.geyser.ping;
import javax.annotation.Nullable;
import java.net.Inet4Address;
import java.net.InetSocketAddress;
/**
@ -34,16 +33,6 @@ import java.net.InetSocketAddress;
*/
public interface IGeyserPingPassthrough {
/**
* Get the MOTD of the server displayed on the multiplayer screen. It uses a fake remote, as the remote isn't important in this context.
*
* @return string of the MOTD
*/
@Nullable
default GeyserPingInfo getPingInformation() {
return this.getPingInformation(new InetSocketAddress(Inet4Address.getLoopbackAddress(), 69));
}
/**
* Get the MOTD of the server displayed on the multiplayer screen
*

Datei anzeigen

@ -198,6 +198,12 @@ public class MappingsReader_v1 extends MappingsReader {
customItemData.renderOffsets(fromJsonNode(tmpNode));
}
if (node.get("tags") instanceof ArrayNode tags) {
Set<String> tagsSet = new ObjectOpenHashSet<>();
tags.forEach(tag -> tagsSet.add(tag.asText()));
customItemData.tags(tagsSet);
}
return customItemData.build();
}

Datei anzeigen

@ -270,6 +270,17 @@ public class CustomItemRegistryPopulator {
.build());
componentBuilder.putCompound("minecraft:display_name", NbtMap.builder().putString("value", customItemData.displayName()).build());
// Add a Geyser tag to the item, allowing Molang queries
addItemTag(componentBuilder, "geyser:is_custom");
// Add other defined tags to the item
Set<String> tags = customItemData.tags();
for (String tag : tags) {
if (tag != null && !tag.isBlank()) {
addItemTag(componentBuilder, tag);
}
}
itemProperties.putBoolean("allow_off_hand", customItemData.allowOffhand());
itemProperties.putBoolean("hand_equipped", displayHandheld);
itemProperties.putInt("max_stack_size", stackSize);
@ -313,7 +324,7 @@ public class CustomItemRegistryPopulator {
.build()
));
componentBuilder.putCompound("minecraft:digger",
componentBuilder.putCompound("minecraft:digger",
NbtMap.builder()
.putList("destroy_speeds", NbtType.COMPOUND, speed)
.putCompound("on_dig", NbtMap.builder()
@ -506,8 +517,19 @@ public class CustomItemRegistryPopulator {
return List.of(xyz.x(), xyz.y(), xyz.z());
}
private static void setItemTag(NbtMapBuilder builder, String tag) {
builder.putList("item_tags", NbtType.STRING, List.of("minecraft:is_" + tag));
@SuppressWarnings("unchecked")
private static void addItemTag(NbtMapBuilder builder, String tag) {
List<String> tagList = (List<String>) builder.get("item_tags");
if (tagList == null) {
builder.putList("item_tags", NbtType.STRING, tag);
} else {
// NbtList is immutable
if (!tagList.contains(tag)) {
tagList = new ArrayList<>(tagList);
tagList.add(tag);
builder.putList("item_tags", NbtType.STRING, tagList);
}
}
}
private static NbtMap xyzToScaleList(float x, float y, float z) {

Datei anzeigen

@ -105,6 +105,7 @@ import org.geysermc.geyser.api.bedrock.camera.CameraShake;
import org.geysermc.geyser.api.connection.GeyserConnection;
import org.geysermc.geyser.api.entity.type.GeyserEntity;
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.AuthType;
import org.geysermc.geyser.api.network.RemoteServer;
@ -128,6 +129,7 @@ import org.geysermc.geyser.item.Items;
import org.geysermc.geyser.level.JavaDimension;
import org.geysermc.geyser.level.WorldManager;
import org.geysermc.geyser.level.physics.CollisionManager;
import org.geysermc.geyser.network.GameProtocol;
import org.geysermc.geyser.network.netty.LocalSession;
import org.geysermc.geyser.registry.Registries;
import org.geysermc.geyser.registry.type.BlockMappings;
@ -391,6 +393,13 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
@Setter
private Entity mouseoverEntity;
/**
* Stores all Java recipes by recipe identifier, and matches them to all possible Bedrock recipe identifiers.
* They are not 1:1, since Bedrock can have multiple recipes for the same Java recipe.
*/
@Setter
private Map<String, List<String>> javaToBedrockRecipeIds;
@Setter
private Int2ObjectMap<GeyserRecipe> craftingRecipes;
private final AtomicInteger lastRecipeNetId;
@ -611,6 +620,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
this.playerInventory = new PlayerInventory();
this.openInventory = null;
this.craftingRecipes = new Int2ObjectOpenHashMap<>();
this.javaToBedrockRecipeIds = new Object2ObjectOpenHashMap<>();
this.lastRecipeNetId = new AtomicInteger(1);
this.spawned = false;
@ -690,6 +700,10 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
gamerulePacket.getGameRules().add(new GameRuleData<>("keepinventory", true));
// Ensure client doesn't try and do anything funky; the server handles this for us
gamerulePacket.getGameRules().add(new GameRuleData<>("spawnradius", 0));
// Recipe unlocking - only needs to be added if 1. it isn't already on via an experiment, or 2. the client is on pre 1.20.10
if (!GameProtocol.isPre1_20_10(this) && !GameProtocol.isUsingExperimentalRecipeUnlocking(this)) {
gamerulePacket.getGameRules().add(new GameRuleData<>("recipesunlock", true));
}
upstream.sendPacket(gamerulePacket);
}
@ -1025,10 +1039,14 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
geyser.getLogger().info(GeyserLocale.getLocaleStringLog("geyser.network.remote.disconnect", authData.name(), remoteServer.address(), disconnectMessage));
}
if (cause != null) {
cause.printStackTrace();
GeyserImpl.getInstance().getLogger().error(cause.getMessage());
// GeyserSession is disconnected via session.disconnect() called indirectly be the server
// This only needs to be "initiated" here when there is an exception, hence the cause clause
GeyserSession.this.disconnect(disconnectMessage);
if (geyser.getConfig().isDebugMode()) {
cause.printStackTrace();
}
}
upstream.disconnect(disconnectMessage);
}
@Override
@ -1055,17 +1073,30 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
public void disconnect(String reason) {
if (!closed) {
loggedIn = false;
// Fire SessionDisconnectEvent
SessionDisconnectEvent disconnectEvent = new SessionDisconnectEvent(this, reason);
geyser.getEventBus().fire(disconnectEvent);
// Disconnect downstream if necessary
if (downstream != null) {
downstream.disconnect(reason);
// No need to disconnect if already closed
if (!downstream.isClosed()) {
downstream.disconnect(reason);
}
} else {
// Downstream's disconnect will fire an event that prints a log message
// Otherwise, we print a message here
String address = geyser.getConfig().isLogPlayerIpAddresses() ? upstream.getAddress().getAddress().toString() : "<IP address withheld>";
geyser.getLogger().info(GeyserLocale.getLocaleStringLog("geyser.network.disconnect", address, reason));
}
// Disconnect upstream if necessary
if (!upstream.isClosed()) {
upstream.disconnect(reason);
upstream.disconnect(disconnectEvent.disconnectReason());
}
// Remove from session manager
geyser.getSessionManager().removeSession(this);
if (authData != null) {
PendingMicrosoftAuthentication.AuthenticationTask task = geyser.getPendingMicrosoftAuthentication().getTask(authData.xuid());
@ -1527,6 +1558,10 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
startGamePacket.setRewindHistorySize(0);
startGamePacket.setServerAuthoritativeBlockBreaking(false);
if (GameProtocol.isUsingExperimentalRecipeUnlocking(this)) {
startGamePacket.getExperiments().add(new ExperimentData("recipe_unlocking", true));
}
upstream.sendPacket(startGamePacket);
}

Datei anzeigen

@ -58,7 +58,7 @@ public class UpstreamSession {
}
public void disconnect(String reason) {
session.disconnect(reason);
this.session.disconnect(reason);
}
/**

Datei anzeigen

@ -162,4 +162,9 @@ public class EntityCache {
public List<Tickable> getTickableEntities() {
return tickableEntities;
}
public void removeAllBossBars() {
bossBars.values().forEach(BossBar::removeBossBar);
bossBars.clear();
}
}

Datei anzeigen

@ -60,6 +60,9 @@ public class SkullBlockEntityTranslator extends BlockEntityTranslator implements
}
builder.put("Rotation", rotation);
builder.put("SkullType", skullVariant);
if (BlockStateValues.isSkullPowered(blockState)) {
builder.putBoolean("MouthMoving", true);
}
}
private static UUID getUUID(CompoundTag owner) {

Datei anzeigen

@ -0,0 +1,79 @@
/*
* Copyright (c) 2019-2023 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.translator.protocol.java;
import com.github.steveice10.mc.protocol.packet.ingame.clientbound.ClientboundRecipePacket;
import org.cloudburstmc.protocol.bedrock.packet.UnlockedRecipesPacket;
import org.geysermc.geyser.network.GameProtocol;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator;
import java.util.ArrayList;
import java.util.List;
@Translator(packet = ClientboundRecipePacket.class)
public class JavaClientboundRecipesTranslator extends PacketTranslator<ClientboundRecipePacket> {
@Override
public void translate(GeyserSession session, ClientboundRecipePacket packet) {
// recipe unlocking does not exist pre 1.20.10
if (GameProtocol.isPre1_20_10(session)) {
return;
}
UnlockedRecipesPacket recipesPacket = new UnlockedRecipesPacket();
switch (packet.getAction()) {
case INIT -> {
recipesPacket.setAction(UnlockedRecipesPacket.ActionType.INITIALLY_UNLOCKED);
recipesPacket.getUnlockedRecipes().addAll(getBedrockRecipes(session, packet.getAlreadyKnownRecipes()));
}
case ADD -> {
recipesPacket.setAction(UnlockedRecipesPacket.ActionType.NEWLY_UNLOCKED);
recipesPacket.getUnlockedRecipes().addAll(getBedrockRecipes(session, packet.getRecipes()));
}
case REMOVE -> {
recipesPacket.setAction(UnlockedRecipesPacket.ActionType.REMOVE_UNLOCKED);
recipesPacket.getUnlockedRecipes().addAll(getBedrockRecipes(session, packet.getRecipes()));
}
}
session.sendUpstreamPacket(recipesPacket);
}
private List<String> getBedrockRecipes(GeyserSession session, String[] javaRecipeIdentifiers) {
List<String> recipes = new ArrayList<>();
for (String javaIdentifier : javaRecipeIdentifiers) {
List<String> bedrockRecipes = session.getJavaToBedrockRecipeIds().get(javaIdentifier);
// Some recipes are not (un)lockable on Bedrock edition, like furnace or stonecutter recipes.
// So we don't store/send these.
if (bedrockRecipes != null) {
recipes.addAll(bedrockRecipes);
}
}
return recipes;
}
}

Datei anzeigen

@ -28,8 +28,11 @@ package org.geysermc.geyser.translator.protocol.java;
import com.github.steveice10.mc.protocol.data.game.entity.player.PlayerSpawnInfo;
import com.github.steveice10.mc.protocol.packet.common.serverbound.ServerboundCustomPayloadPacket;
import com.github.steveice10.mc.protocol.packet.ingame.clientbound.ClientboundLoginPacket;
import org.cloudburstmc.math.vector.Vector3f;
import org.cloudburstmc.protocol.bedrock.data.GameRuleData;
import org.cloudburstmc.protocol.bedrock.data.LevelEvent;
import org.cloudburstmc.protocol.bedrock.packet.GameRulesChangedPacket;
import org.cloudburstmc.protocol.bedrock.packet.LevelEventPacket;
import org.cloudburstmc.protocol.bedrock.packet.SetPlayerGameTypePacket;
import org.geysermc.floodgate.pluginmessage.PluginMessageChannels;
import org.geysermc.geyser.api.network.AuthType;
@ -64,12 +67,36 @@ public class JavaLoginTranslator extends PacketTranslator<ClientboundLoginPacket
DimensionUtils.switchDimension(session, fakeDim);
session.getWorldCache().removeScoreboard();
// Remove all bossbars
session.getEntityCache().removeAllBossBars();
// Remove extra hearts, hunger, etc.
entity.getAttributes().clear();
entity.resetMetadata();
// Reset weather
if (session.isRaining()) {
LevelEventPacket stopRainPacket = new LevelEventPacket();
stopRainPacket.setType(LevelEvent.STOP_RAINING);
stopRainPacket.setData(0);
stopRainPacket.setPosition(Vector3f.ZERO);
session.sendUpstreamPacket(stopRainPacket);
session.setRaining(false);
}
if (session.isThunder()) {
LevelEventPacket stopThunderPacket = new LevelEventPacket();
stopThunderPacket.setType(LevelEvent.STOP_THUNDERSTORM);
stopThunderPacket.setData(0);
stopThunderPacket.setPosition(Vector3f.ZERO);
session.sendUpstreamPacket(stopThunderPacket);
session.setThunder(false);
}
}
session.setWorldName(spawnInfo.getWorldName());
session.setLevels(packet.getWorldNames());
session.setGameMode(spawnInfo.getGameMode());
String newDimension = spawnInfo.getDimension();
boolean needsSpawnPacket = !session.isSentSpawnPacket();
@ -81,9 +108,7 @@ public class JavaLoginTranslator extends PacketTranslator<ClientboundLoginPacket
// It is now safe to send these packets
session.getUpstream().sendPostStartGamePackets();
}
if (!needsSpawnPacket) {
} else {
SetPlayerGameTypePacket playerGameTypePacket = new SetPlayerGameTypePacket();
playerGameTypePacket.setGamemode(EntityUtils.toBedrockGamemode(spawnInfo.getGameMode()).ordinal());
session.sendUpstreamPacket(playerGameTypePacket);

Datei anzeigen

@ -44,6 +44,7 @@ import org.cloudburstmc.protocol.bedrock.data.inventory.crafting.recipe.RecipeDa
import org.cloudburstmc.protocol.bedrock.data.inventory.crafting.recipe.SmithingTrimRecipeData;
import org.cloudburstmc.protocol.bedrock.data.inventory.descriptor.DefaultDescriptor;
import org.cloudburstmc.protocol.bedrock.data.inventory.descriptor.ItemDescriptorWithCount;
import org.cloudburstmc.protocol.bedrock.data.inventory.descriptor.ItemTagDescriptor;
import org.cloudburstmc.protocol.bedrock.packet.CraftingDataPacket;
import org.cloudburstmc.protocol.bedrock.packet.TrimDataPacket;
import org.geysermc.geyser.GeyserImpl;
@ -67,7 +68,6 @@ import static org.geysermc.geyser.util.InventoryUtils.LAST_RECIPE_NET_ID;
/**
* Used to send all valid recipes from Java to Bedrock.
*
* Bedrock REQUIRES a CraftingDataPacket to be sent in order to craft anything.
*/
@Translator(packet = ClientboundUpdateRecipesPacket.class)
@ -94,17 +94,27 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator<ClientboundUpd
"minecraft:netherite_boots"
);
/**
* Fixes https://github.com/GeyserMC/Geyser/issues/3784 by using item tags where applicable instead of group IDs
* Item Tags allow mixing ingredients, and theoretically, adding item tags to custom items should also include them.
*/
private static final Map<String, String> RECIPE_TAGS = Map.of(
"minecraft:wood", "minecraft:logs",
"minecraft:wooden_slab", "minecraft:wooden_slabs",
"minecraft:planks", "minecraft:planks");
@Override
public void translate(GeyserSession session, ClientboundUpdateRecipesPacket packet) {
Map<RecipeType, List<RecipeData>> recipeTypes = Registries.CRAFTING_DATA.forVersion(session.getUpstream().getProtocolVersion());
// Get the last known network ID (first used for the pregenerated recipes) and increment from there.
int netId = InventoryUtils.LAST_RECIPE_NET_ID + 1;
boolean sendTrimRecipes = false;
Map<String, List<String>> recipeIDs = session.getJavaToBedrockRecipeIds();
Int2ObjectMap<GeyserRecipe> recipeMap = new Int2ObjectOpenHashMap<>(Registries.RECIPES.forVersion(session.getUpstream().getProtocolVersion()));
Int2ObjectMap<List<StoneCuttingRecipeData>> unsortedStonecutterData = new Int2ObjectOpenHashMap<>();
CraftingDataPacket craftingDataPacket = new CraftingDataPacket();
craftingDataPacket.setCleanRecipes(true);
for (Recipe recipe : packet.getRecipes()) {
switch (recipe.getType()) {
case CRAFTING_SHAPELESS -> {
@ -121,12 +131,15 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator<ClientboundUpd
continue;
}
List<String> bedrockRecipeIDs = new ArrayList<>();
for (ItemDescriptorWithCount[] inputs : inputCombinations) {
UUID uuid = UUID.randomUUID();
bedrockRecipeIDs.add(uuid.toString());
craftingDataPacket.getCraftingData().add(org.cloudburstmc.protocol.bedrock.data.inventory.crafting.recipe.ShapelessRecipeData.shapeless(uuid.toString(),
Arrays.asList(inputs), Collections.singletonList(output), uuid, "crafting_table", 0, netId));
recipeMap.put(netId++, new GeyserShapelessRecipe(shapelessRecipeData));
}
addRecipeIdentifier(session, recipe.getIdentifier(), bedrockRecipeIDs);
}
case CRAFTING_SHAPED -> {
ShapedRecipeData shapedRecipeData = (ShapedRecipeData) recipe.getData();
@ -141,13 +154,17 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator<ClientboundUpd
if (inputCombinations == null) {
continue;
}
List<String> bedrockRecipeIDs = new ArrayList<>();
for (ItemDescriptorWithCount[] inputs : inputCombinations) {
UUID uuid = UUID.randomUUID();
bedrockRecipeIDs.add(uuid.toString());
craftingDataPacket.getCraftingData().add(org.cloudburstmc.protocol.bedrock.data.inventory.crafting.recipe.ShapedRecipeData.shaped(uuid.toString(),
shapedRecipeData.getWidth(), shapedRecipeData.getHeight(), Arrays.asList(inputs),
Collections.singletonList(output), uuid, "crafting_table", 0, netId));
recipeMap.put(netId++, new GeyserShapedRecipe(shapedRecipeData));
}
addRecipeIdentifier(session, recipe.getIdentifier(), bedrockRecipeIDs);
}
case STONECUTTING -> {
StoneCuttingRecipeData stoneCuttingData = (StoneCuttingRecipeData) recipe.getData();
@ -157,8 +174,8 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator<ClientboundUpd
data = new ArrayList<>();
unsortedStonecutterData.put(ingredient.getId(), data);
}
data.add(stoneCuttingData);
// Save for processing after all recipes have been received
data.add(stoneCuttingData);
}
case SMITHING_TRANSFORM -> {
SmithingTransformRecipeData data = (SmithingTransformRecipeData) recipe.getData();
@ -173,21 +190,29 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator<ClientboundUpd
for (ItemStack addition : data.getAddition().getOptions()) {
ItemDescriptorWithCount bedrockAddition = ItemDescriptorWithCount.fromItem(ItemTranslator.translateToBedrock(session, addition));
String id = recipe.getIdentifier();
// Note: vanilla inputs use aux value of Short.MAX_VALUE
craftingDataPacket.getCraftingData().add(org.cloudburstmc.protocol.bedrock.data.inventory.crafting.recipe.SmithingTransformRecipeData.of(recipe.getIdentifier(),
bedrockTemplate, bedrockBase, bedrockAddition, output, "smithing_table", netId++));
craftingDataPacket.getCraftingData().add(org.cloudburstmc.protocol.bedrock.data.inventory.crafting.recipe.SmithingTransformRecipeData.of(id,
bedrockTemplate, bedrockBase, bedrockAddition, output, "smithing_table", netId++));
recipeIDs.put(id, new ArrayList<>(Collections.singletonList(id)));
}
}
}
}
case SMITHING_TRIM -> {
sendTrimRecipes = true;
// ignored currently - see below
}
case CRAFTING_DECORATED_POT -> {
// Paper 1.20 seems to send only one recipe, which seems to be hardcoded to include all recipes.
// We can send the equivalent Bedrock MultiRecipe! :)
craftingDataPacket.getCraftingData().add(MultiRecipeData.of(UUID.fromString("685a742a-c42e-4a4e-88ea-5eb83fc98e5b"), netId++));
}
default -> {
List<RecipeData> craftingData = recipeTypes.get(recipe.getType());
if (craftingData != null) {
addSpecialRecipesIdentifiers(session, recipe, craftingData);
craftingDataPacket.getCraftingData().addAll(craftingData);
}
}
@ -218,14 +243,15 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator<ClientboundUpd
continue;
}
UUID uuid = UUID.randomUUID();
// We need to register stonecutting recipes so they show up on Bedrock
// We need to register stonecutting recipes, so they show up on Bedrock
craftingDataPacket.getCraftingData().add(org.cloudburstmc.protocol.bedrock.data.inventory.crafting.recipe.ShapelessRecipeData.shapeless(uuid.toString(),
Collections.singletonList(descriptor), Collections.singletonList(output), uuid, "stonecutter", 0, netId));
// Save the recipe list for reference when crafting
// Add the net ID as the key and the button required + output for the value
stonecutterRecipeMap.put(netId++, new GeyserStonecutterData(buttonId++, javaOutput));
// Currently, stone cutter recipes are not locked/unlocked on Bedrock; so no need to cache their identifiers.
}
}
@ -251,6 +277,38 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator<ClientboundUpd
session.sendUpstreamPacket(craftingDataPacket);
session.setCraftingRecipes(recipeMap);
session.setStonecutterRecipes(stonecutterRecipeMap);
session.setJavaToBedrockRecipeIds(recipeIDs);
}
private void addSpecialRecipesIdentifiers(GeyserSession session, Recipe recipe, List<RecipeData> craftingData) {
String javaRecipeID = recipe.getIdentifier();
switch (recipe.getType()) {
case CRAFTING_SPECIAL_BOOKCLONING, CRAFTING_SPECIAL_REPAIRITEM, CRAFTING_SPECIAL_MAPEXTENDING, CRAFTING_SPECIAL_MAPCLONING:
// We do not want to (un)lock these, since BDS does not do it for MultiRecipes
return;
case CRAFTING_SPECIAL_SHULKERBOXCOLORING:
// BDS (un)locks the dyeing with the shulker box recipe, Java never - we want BDS behavior for ease of use
javaRecipeID = "minecraft:shulker_box";
break;
case CRAFTING_SPECIAL_TIPPEDARROW:
// similar as above
javaRecipeID = "minecraft:arrow";
break;
}
List<String> bedrockRecipeIDs = new ArrayList<>();
// defined in the recipes.json mappings file: Only tipped arrows use shaped recipes, we need the cast for the identifier
if (recipe.getType() == RecipeType.CRAFTING_SPECIAL_TIPPEDARROW) {
for (RecipeData data : craftingData) {
bedrockRecipeIDs.add(((org.cloudburstmc.protocol.bedrock.data.inventory.crafting.recipe.ShapedRecipeData) data).getId());
}
} else {
for (RecipeData data : craftingData) {
bedrockRecipeIDs.add(((org.cloudburstmc.protocol.bedrock.data.inventory.crafting.recipe.ShapelessRecipeData) data).getId());
}
}
addRecipeIdentifier(session, javaRecipeID, bedrockRecipeIDs);
}
//TODO: rewrite
@ -277,6 +335,13 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator<ClientboundUpd
for (Map.Entry<GroupedItem, List<ItemDescriptorWithCount>> entry : groupedByIds.entrySet()) {
if (entry.getValue().size() > 1) {
GroupedItem groupedItem = entry.getKey();
String recipeTag = RECIPE_TAGS.get(groupedItem.id.getIdentifier());
if (recipeTag != null) {
optionSet.add(new ItemDescriptorWithCount(new ItemTagDescriptor(recipeTag), groupedItem.count));
continue;
}
int idCount = 0;
//not optimal
for (ItemMapping mapping : session.getItemMappings().getItems()) {
@ -337,6 +402,10 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator<ClientboundUpd
return combinations;
}
private void addRecipeIdentifier(GeyserSession session, String javaIdentifier, List<String> bedrockIdentifiers) {
session.getJavaToBedrockRecipeIds().computeIfAbsent(javaIdentifier, k -> new ArrayList<>()).addAll(bedrockIdentifiers);
}
@EqualsAndHashCode
@AllArgsConstructor
private static class GroupedItem {

Datei anzeigen

@ -91,9 +91,6 @@ command-suggestions: true
# The following three options enable "ping passthrough" - the MOTD, player count and/or protocol name gets retrieved from the Java server.
# Relay the MOTD from the remote server to Bedrock players.
passthrough-motd: false
# Relay the protocol name (e.g. BungeeCord [X.X], Paper 1.X) - only really useful when using a custom protocol name!
# This will also show up on sites like MCSrvStatus. <mcsrvstat.us>
passthrough-protocol-name: false
# Relay the player count and max players from the remote server to Bedrock players.
passthrough-player-counts: false
# Enable LEGACY ping passthrough. There is no need to enable this unless your MOTD or player count does not appear properly.