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:
Commit
b1f055d31f
28
Jenkinsfile
vendored
28
Jenkinsfile
vendored
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -185,7 +185,7 @@ public interface CustomBlockComponents {
|
||||
|
||||
Builder placeAir(boolean placeAir);
|
||||
|
||||
Builder tags(Set<String> tags);
|
||||
Builder tags(@Nullable Set<String> tags);
|
||||
|
||||
CustomBlockComponents build();
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -239,6 +239,9 @@ public interface NonVanillaCustomItemData extends CustomItemData {
|
||||
@Override
|
||||
Builder renderOffsets(@Nullable CustomRenderOffsets renderOffsets);
|
||||
|
||||
@Override
|
||||
Builder tags(@Nullable Set<String> tags);
|
||||
|
||||
NonVanillaCustomItemData build();
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
|
@ -57,9 +57,6 @@ public interface GeyserConfiguration {
|
||||
@JsonIgnore
|
||||
boolean isPassthroughMotd();
|
||||
|
||||
@JsonIgnore
|
||||
boolean isPassthroughProtocolName();
|
||||
|
||||
@JsonIgnore
|
||||
boolean isPassthroughPlayerCounts();
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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()));
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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.
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
*
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
*
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -58,7 +58,7 @@ public class UpstreamSession {
|
||||
}
|
||||
|
||||
public void disconnect(String reason) {
|
||||
session.disconnect(reason);
|
||||
this.session.disconnect(reason);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -162,4 +162,9 @@ public class EntityCache {
|
||||
public List<Tickable> getTickableEntities() {
|
||||
return tickableEntities;
|
||||
}
|
||||
|
||||
public void removeAllBossBars() {
|
||||
bossBars.values().forEach(BossBar::removeBossBar);
|
||||
bossBars.clear();
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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 {
|
||||
|
@ -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.
|
||||
|
Laden…
In neuem Issue referenzieren
Einen Benutzer sperren