diff --git a/src/main/java/de/florianmichael/viafabricplus/protocolhack/provider/vialoadingbase/ViaFabricPlusVLBBaseVersionProvider.java b/src/main/java/de/florianmichael/viafabricplus/protocolhack/provider/vialoadingbase/ViaFabricPlusVLBBaseVersionProvider.java index c8945f06..04ec8659 100644 --- a/src/main/java/de/florianmichael/viafabricplus/protocolhack/provider/vialoadingbase/ViaFabricPlusVLBBaseVersionProvider.java +++ b/src/main/java/de/florianmichael/viafabricplus/protocolhack/provider/vialoadingbase/ViaFabricPlusVLBBaseVersionProvider.java @@ -17,15 +17,139 @@ */ package de.florianmichael.viafabricplus.protocolhack.provider.vialoadingbase; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; import com.viaversion.viaversion.api.connection.UserConnection; +import com.viaversion.viaversion.api.protocol.version.ProtocolVersion; +import de.florianmichael.viafabricplus.ViaFabricPlus; import de.florianmichael.viafabricplus.protocolhack.ProtocolHack; +import de.florianmichael.viafabricplus.settings.groups.GeneralSettings; +import de.florianmichael.vialoadingbase.ViaLoadingBase; +import de.florianmichael.vialoadingbase.model.ComparableProtocolVersion; import de.florianmichael.vialoadingbase.provider.VLBBaseVersionProvider; +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.*; +import io.netty.channel.epoll.Epoll; +import io.netty.channel.epoll.EpollSocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.timeout.ReadTimeoutHandler; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ServerAddress; +import net.minecraft.network.ClientConnection; +import net.minecraft.network.NetworkSide; +import net.minecraft.network.NetworkState; +import net.minecraft.network.listener.ClientQueryPacketListener; +import net.minecraft.network.packet.c2s.handshake.HandshakeC2SPacket; +import net.minecraft.network.packet.c2s.query.QueryRequestC2SPacket; +import net.minecraft.network.packet.s2c.query.QueryPongS2CPacket; +import net.minecraft.network.packet.s2c.query.QueryResponseS2CPacket; +import net.minecraft.text.Text; +import net.minecraft.util.Lazy; +import org.jetbrains.annotations.NotNull; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; public class ViaFabricPlusVLBBaseVersionProvider extends VLBBaseVersionProvider { + // Based on https://github.com/ViaVersion/ViaFabric/blob/main/viafabric-mc119/src/main/java/com/viaversion/fabric/mc119/service/ProtocolAutoDetector.java + private final static LoadingCache> AUTO_DETECTION_CACHE = CacheBuilder.newBuilder(). + expireAfterWrite(30, TimeUnit.SECONDS). + build(CacheLoader.from(address -> { + CompletableFuture future = new CompletableFuture<>(); + + try { + final ClientConnection clientConnection = new ClientConnection(NetworkSide.CLIENTBOUND); + final boolean useEpoll = Epoll.isAvailable() && MinecraftClient.getInstance().options.shouldUseNativeTransport(); + + final Class class_ = useEpoll ? EpollSocketChannel.class : NioSocketChannel.class; + final Lazy lazy = useEpoll ? ClientConnection.EPOLL_CLIENT_IO_GROUP : ClientConnection.CLIENT_IO_GROUP; + + final ChannelFuture channelFuture = new Bootstrap().group((EventLoopGroup) lazy.get()).handler(new ChannelInitializer<>() { + protected void initChannel(@NotNull Channel channel) { + try { + channel.config().setOption(ChannelOption.TCP_NODELAY, true); + channel.config().setOption(ChannelOption.IP_TOS, 0x18); + } catch (ChannelException ignored) { + } + + ChannelPipeline channelPipeline = channel.pipeline().addLast("timeout", new ReadTimeoutHandler(30)); + ClientConnection.addHandlers(channelPipeline, NetworkSide.CLIENTBOUND); + channelPipeline.addLast("packet_handler", clientConnection); + } + }).channel(class_).connect(address); + + channelFuture.addListener(future1 -> { + if (!future1.isSuccess()) { + future.completeExceptionally(future1.cause()); + } else { + channelFuture.channel().eventLoop().execute(() -> { // needs to execute after channel init + clientConnection.setPacketListener(new ClientQueryPacketListener() { + @Override + public void onResponse(QueryResponseS2CPacket packet) { + if (packet.metadata() != null && packet.metadata().version().isPresent()) { + final ProtocolVersion version = ViaLoadingBase.fromProtocolId(packet.metadata().version().get().protocolVersion()); + future.complete(version); + + ViaFabricPlus.LOGGER.info("Auto-detected " + version + " for " + address); + } else { + future.completeExceptionally(new IllegalArgumentException("Null version in query response")); + } + clientConnection.disconnect(Text.empty()); + } + + @Override + public void onPong(QueryPongS2CPacket packet) { + clientConnection.disconnect(Text.literal("Pong not requested!")); + } + + @Override + public void onDisconnected(Text reason) { + future.completeExceptionally(new IllegalStateException(reason.getString())); + } + + @Override + public boolean isConnectionOpen() { + return channelFuture.channel().isOpen(); + } + }); + + clientConnection.send(new HandshakeC2SPacket(address.getHostString(), address.getPort(), NetworkState.STATUS)); + clientConnection.send(new QueryRequestC2SPacket()); + }); + } + }); + } catch (Throwable throwable) { // You never know... + future.completeExceptionally(throwable); + } + + return future; + })); + @Override public int getClosestServerProtocol(UserConnection connection) throws Exception { if (connection.isClientSide()) { + if (GeneralSettings.INSTANCE.autoDetectVersion.getValue()) { + final SocketAddress target = connection.getChannel().remoteAddress(); + if (target instanceof final InetSocketAddress socketAddress) { + AUTO_DETECTION_CACHE.get(socketAddress).whenComplete((version, throwable) -> { + if (throwable != null) { + throwable.printStackTrace(); + return; + } + if (version != null) { + final ComparableProtocolVersion remapped = ViaLoadingBase.fromProtocolId(version.getVersion()); + if (remapped != null) { + ProtocolHack.getForcedVersions().put(socketAddress, remapped); + } + } + }); + } + } return ProtocolHack.getTargetVersion(connection.getChannel()).getVersion(); } return super.getClosestServerProtocol(connection); diff --git a/src/main/java/de/florianmichael/viafabricplus/settings/groups/GeneralSettings.java b/src/main/java/de/florianmichael/viafabricplus/settings/groups/GeneralSettings.java index cf0900f1..f68435fd 100644 --- a/src/main/java/de/florianmichael/viafabricplus/settings/groups/GeneralSettings.java +++ b/src/main/java/de/florianmichael/viafabricplus/settings/groups/GeneralSettings.java @@ -37,6 +37,7 @@ public class GeneralSettings extends SettingGroup { public final BooleanSetting showSuperSecretSettings = new BooleanSetting(this, Text.translatable("general.viafabricplus.secret"), true); public final BooleanSetting showExtraInformationInDebugHud = new BooleanSetting(this, Text.translatable("general.viafabricplus.extrainformation"), true); public final BooleanSetting showClassicLoadingProgressInConnectScreen = new BooleanSetting(this, Text.translatable("general.viafabricplus.classicloading"), true); + public final BooleanSetting autoDetectVersion = new BooleanSetting(this, Text.translatable("general.viafabricplus.autodetect"), false); public GeneralSettings() { super("General"); diff --git a/src/main/resources/assets/viafabricplus/lang/en_us.json b/src/main/resources/assets/viafabricplus/lang/en_us.json index f2648168..028868dd 100644 --- a/src/main/resources/assets/viafabricplus/lang/en_us.json +++ b/src/main/resources/assets/viafabricplus/lang/en_us.json @@ -20,6 +20,7 @@ "general.viafabricplus.creative": "Remove not available items from creative tab", "general.viafabricplus.classiccommands": "Allow classic protocol command usage", "general.viafabricplus.protocolsync": "Automatically change Settings based on the current version", + "general.viafabricplus.autodetect": "Auto detect version", "experimental.viafabricplus.chunkborderfix": "Fix Chunk borders", "experimental.viafabricplus.watermovement": "Water movement edge detection", diff --git a/src/main/resources/viafabricplus.accesswidener b/src/main/resources/viafabricplus.accesswidener index 2e89d2bc..d04de67d 100644 --- a/src/main/resources/viafabricplus.accesswidener +++ b/src/main/resources/viafabricplus.accesswidener @@ -8,5 +8,7 @@ accessible field net/minecraft/client/network/ServerAddress INVALID Lnet/minecra accessible field net/minecraft/client/network/ServerAddress hostAndPort Lcom/google/common/net/HostAndPort; accessible field net/minecraft/client/MinecraftClient fontManager Lnet/minecraft/client/font/FontManager; accessible field net/minecraft/client/font/FontManager fontStorages Ljava/util/Map; +accessible field net/minecraft/network/ClientConnection EPOLL_CLIENT_IO_GROUP Lnet/minecraft/util/Lazy; +accessible field net/minecraft/network/ClientConnection LOCAL_CLIENT_IO_GROUP Lnet/minecraft/util/Lazy; accessible class net/minecraft/client/gui/screen/GameModeSelectionScreen$GameModeSelection