diff --git a/api/src/main/java/com/velocitypowered/api/event/player/PlayerModInfoEvent.java b/api/src/main/java/com/velocitypowered/api/event/player/PlayerModInfoEvent.java new file mode 100644 index 000000000..9144fc441 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/event/player/PlayerModInfoEvent.java @@ -0,0 +1,26 @@ +package com.velocitypowered.api.event.player; + +import com.google.common.base.Preconditions; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.util.ModInfo; + +/** + * This event is fired when the players ModInfo is changed. + */ +public final class PlayerModInfoEvent { + private final Player player; + private final ModInfo modInfo; + + public PlayerModInfoEvent(Player player, ModInfo modInfo) { + this.player = Preconditions.checkNotNull(player, "player"); + this.modInfo = Preconditions.checkNotNull(modInfo, "modInfo"); + } + + public Player getPlayer() { + return player; + } + + public ModInfo getModInfo() { + return modInfo; + } +} diff --git a/api/src/main/java/com/velocitypowered/api/proxy/Player.java b/api/src/main/java/com/velocitypowered/api/proxy/Player.java index 9650c0f36..57300de66 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/Player.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/Player.java @@ -8,6 +8,7 @@ import com.velocitypowered.api.proxy.server.RegisteredServer; import com.velocitypowered.api.util.GameProfile; import com.velocitypowered.api.proxy.player.TabList; import com.velocitypowered.api.util.MessagePosition; +import com.velocitypowered.api.util.ModInfo; import com.velocitypowered.api.util.title.Title; import java.util.List; @@ -44,6 +45,12 @@ public interface Player extends CommandSource, InboundConnection, ChannelMessage * @return the settings */ PlayerSettings getPlayerSettings(); + + /** + * Returns the player's mod info if they have a modded client. + * @return an {@link Optional} the mod info. which may be empty + */ + Optional getModInfo(); /** * Returns the current player's ping diff --git a/api/src/main/java/com/velocitypowered/api/proxy/server/ServerPing.java b/api/src/main/java/com/velocitypowered/api/proxy/server/ServerPing.java index 4dc47c33d..bcd964572 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/server/ServerPing.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/server/ServerPing.java @@ -3,6 +3,7 @@ package com.velocitypowered.api.proxy.server; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.velocitypowered.api.util.Favicon; +import com.velocitypowered.api.util.ModInfo; import net.kyori.text.Component; import org.checkerframework.checker.nullness.qual.Nullable; @@ -16,13 +17,13 @@ public final class ServerPing { private final Players players; private final Component description; private final @Nullable Favicon favicon; - private final ModInfo modinfo; + private final @Nullable ModInfo modinfo; public ServerPing(Version version, @Nullable Players players, Component description, @Nullable Favicon favicon) { this(version, players, description, favicon, ModInfo.DEFAULT); } - public ServerPing(Version version, @Nullable Players players, Component description, @Nullable Favicon favicon, ServerPing.@Nullable ModInfo modinfo) { + public ServerPing(Version version, @Nullable Players players, Component description, @Nullable Favicon favicon, @Nullable ModInfo modinfo) { this.version = Preconditions.checkNotNull(version, "version"); this.players = players; this.description = Preconditions.checkNotNull(description, "description"); @@ -74,8 +75,8 @@ public final class ServerPing { builder.favicon = favicon; builder.nullOutModinfo = modinfo == null; if (modinfo != null) { - builder.modType = modinfo.type; - builder.mods.addAll(modinfo.modList); + builder.modType = modinfo.getType(); + builder.mods.addAll(modinfo.getMods()); } return builder; } @@ -93,7 +94,7 @@ public final class ServerPing { private int maximumPlayers; private final List samplePlayers = new ArrayList<>(); private String modType; - private final List mods = new ArrayList<>(); + private final List mods = new ArrayList<>(); private Component description; private Favicon favicon; private boolean nullOutPlayers; @@ -128,7 +129,7 @@ public final class ServerPing { return this; } - public Builder mods(Mod... mods) { + public Builder mods(ModInfo.Mod... mods) { this.mods.addAll(Arrays.asList(mods)); return this; } @@ -196,7 +197,7 @@ public final class ServerPing { return modType; } - public List getMods() { + public List getMods() { return mods; } @@ -301,58 +302,4 @@ public final class ServerPing { '}'; } } - - public static final class ModInfo { - public static final ModInfo DEFAULT = new ModInfo("FML", ImmutableList.of()); - - private final String type; - private final List modList; - - public ModInfo(String type, List modList) { - this.type = Preconditions.checkNotNull(type, "type"); - this.modList = ImmutableList.copyOf(modList); - } - - public String getType() { - return type; - } - - public List getMods() { - return modList; - } - - @Override - public String toString() { - return "ModInfo{" + - "type='" + type + '\'' + - ", modList=" + modList + - '}'; - } - } - - public static final class Mod { - private final String id; - private final String version; - - public Mod(String id, String version) { - this.id = Preconditions.checkNotNull(id, "id"); - this.version = Preconditions.checkNotNull(version, "version"); - } - - public String getId() { - return id; - } - - public String getVersion() { - return version; - } - - @Override - public String toString() { - return "Mod{" + - "id='" + id + '\'' + - ", version='" + version + '\'' + - '}'; - } - } } diff --git a/api/src/main/java/com/velocitypowered/api/util/ModInfo.java b/api/src/main/java/com/velocitypowered/api/util/ModInfo.java new file mode 100644 index 000000000..bdf7c9423 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/util/ModInfo.java @@ -0,0 +1,60 @@ +package com.velocitypowered.api.util; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; + +import java.util.List; + +public final class ModInfo { + public static final ModInfo DEFAULT = new ModInfo("FML", ImmutableList.of()); + + private final String type; + private final List modList; + + public ModInfo(String type, List modList) { + this.type = Preconditions.checkNotNull(type, "type"); + this.modList = ImmutableList.copyOf(modList); + } + + public String getType() { + return type; + } + + public List getMods() { + return modList; + } + + @Override + public String toString() { + return "ModInfo{" + + "type='" + type + '\'' + + ", modList=" + modList + + '}'; + } + + public static final class Mod { + private final String id; + private final String version; + + public Mod(String id, String version) { + this.id = Preconditions.checkNotNull(id, "id"); + this.version = Preconditions.checkNotNull(version, "version"); + } + + public String getId() { + return id; + } + + public String getVersion() { + return version; + } + + @Override + public String toString() { + return "Mod{" + + "id='" + id + '\'' + + ", version='" + version + '\'' + + '}'; + } + } +} \ No newline at end of file diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java index 9f7001bb3..acfedf1cd 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java @@ -1,10 +1,9 @@ package com.velocitypowered.proxy.connection.client; -import com.velocitypowered.api.event.connection.DisconnectEvent; import com.velocitypowered.api.event.connection.PluginMessageEvent; import com.velocitypowered.api.event.player.PlayerChatEvent; import com.velocitypowered.api.proxy.messages.ChannelIdentifier; -import com.velocitypowered.api.proxy.player.TabList; +import com.velocitypowered.api.util.ModInfo; import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.connection.VelocityConstants; @@ -13,7 +12,6 @@ import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.ProtocolConstants; import com.velocitypowered.proxy.protocol.packet.*; import com.velocitypowered.proxy.protocol.util.PluginMessageUtil; -import com.velocitypowered.proxy.tablist.VelocityTabList; import com.velocitypowered.proxy.util.ThrowableUtils; import io.netty.buffer.ByteBuf; import net.kyori.text.TextComponent; @@ -147,6 +145,10 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { player.getConnectedServer().getConnection().write(PluginMessageUtil.rewriteMCBrand(packet)); } else if (player.getConnectedServer().isLegacyForge() && !player.getConnectedServer().hasCompletedJoin()) { if (packet.getChannel().equals(VelocityConstants.FORGE_LEGACY_HANDSHAKE_CHANNEL)) { + if (!player.getModInfo().isPresent()) { + PluginMessageUtil.readModList(packet).ifPresent(mods -> player.setModInfo(new ModInfo("FML", mods))); + } + // Always forward the FML handshake to the remote server. player.getConnectedServer().getConnection().write(packet); } else { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java index 4e3afb26f..a388bc099 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java @@ -4,6 +4,7 @@ import com.google.common.base.Preconditions; import com.google.gson.JsonObject; import com.velocitypowered.api.event.connection.DisconnectEvent; import com.velocitypowered.api.event.player.KickedFromServerEvent; +import com.velocitypowered.api.event.player.PlayerModInfoEvent; import com.velocitypowered.api.event.player.PlayerSettingsChangedEvent; import com.velocitypowered.api.event.player.ServerPreConnectEvent; import com.velocitypowered.api.permission.PermissionFunction; @@ -16,8 +17,8 @@ import com.velocitypowered.api.proxy.messages.ChannelIdentifier; import com.velocitypowered.api.proxy.player.PlayerSettings; import com.velocitypowered.api.proxy.server.RegisteredServer; import com.velocitypowered.api.util.GameProfile; -import com.velocitypowered.api.proxy.player.TabList; import com.velocitypowered.api.util.MessagePosition; +import com.velocitypowered.api.util.ModInfo; import com.velocitypowered.api.util.title.TextTitle; import com.velocitypowered.api.util.title.Title; import com.velocitypowered.api.util.title.Titles; @@ -31,9 +32,6 @@ import com.velocitypowered.proxy.connection.util.ConnectionRequestResults; import com.velocitypowered.proxy.protocol.ProtocolConstants; import com.velocitypowered.proxy.protocol.packet.*; import com.velocitypowered.proxy.server.VelocityRegisteredServer; -import com.velocitypowered.proxy.protocol.packet.Chat; -import com.velocitypowered.proxy.protocol.packet.ClientSettings; -import com.velocitypowered.proxy.protocol.packet.PluginMessage; import com.velocitypowered.proxy.tablist.VelocityTabList; import com.velocitypowered.proxy.util.ThrowableUtils; import net.kyori.text.Component; @@ -69,6 +67,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { private VelocityServerConnection connectedServer; private VelocityServerConnection connectionInFlight; private PlayerSettings settings; + private ModInfo modInfo; private final VelocityTabList tabList; private final VelocityServer server; @@ -120,7 +119,16 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { this.settings = new ClientSettingsWrapper(settings); server.getEventManager().fireAndForget(new PlayerSettingsChangedEvent(this, this.settings)); } - + + public Optional getModInfo() { + return Optional.ofNullable(modInfo); + } + + void setModInfo(ModInfo modInfo) { + this.modInfo = modInfo; + server.getEventManager().fireAndForget(new PlayerModInfoEvent(this, this.modInfo)); + } + @Override public InetSocketAddress getRemoteAddress() { return (InetSocketAddress) connection.getRemoteAddress(); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/StatusSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/StatusSessionHandler.java index 0ccd87511..48ec7e128 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/StatusSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/StatusSessionHandler.java @@ -4,6 +4,7 @@ import com.google.common.collect.ImmutableList; import com.velocitypowered.api.event.proxy.ProxyPingEvent; import com.velocitypowered.api.proxy.InboundConnection; import com.velocitypowered.api.proxy.server.ServerPing; +import com.velocitypowered.api.util.ModInfo; import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.config.VelocityConfiguration; import com.velocitypowered.proxy.connection.MinecraftConnection; @@ -42,7 +43,7 @@ public class StatusSessionHandler implements MinecraftSessionHandler { new ServerPing.Players(server.getPlayerCount(), configuration.getShowMaxPlayers(), ImmutableList.of()), configuration.getMotdComponent(), configuration.getFavicon().orElse(null), - configuration.isAnnounceForge() ? ServerPing.ModInfo.DEFAULT : null + configuration.isAnnounceForge() ? ModInfo.DEFAULT : null ); ProxyPingEvent event = new ProxyPingEvent(inboundWrapper, initialPing); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/PluginMessageUtil.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/PluginMessageUtil.java index 884f2a0a6..bf2f48cf9 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/PluginMessageUtil.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/PluginMessageUtil.java @@ -2,6 +2,9 @@ package com.velocitypowered.proxy.protocol.util; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import com.velocitypowered.api.util.ModInfo; +import com.velocitypowered.proxy.connection.VelocityConstants; import com.velocitypowered.proxy.protocol.ProtocolConstants; import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.packet.PluginMessage; @@ -11,6 +14,7 @@ import io.netty.buffer.Unpooled; import java.nio.charset.StandardCharsets; import java.util.Collection; import java.util.List; +import java.util.Optional; public class PluginMessageUtil { public static final String BRAND_CHANNEL_LEGACY = "MC|Brand"; @@ -76,4 +80,32 @@ public class PluginMessageUtil { newMsg.setData(rewrittenData); return newMsg; } + + public static Optional> readModList(PluginMessage message) { + Preconditions.checkNotNull(message, "message"); + Preconditions.checkArgument(message.getChannel().equals(VelocityConstants.FORGE_LEGACY_HANDSHAKE_CHANNEL), + "message is not a FML HS plugin message"); + + ByteBuf byteBuf = Unpooled.wrappedBuffer(message.getData()); + try { + byte discriminator = byteBuf.readByte(); + + if (discriminator == 2) { + ImmutableList.Builder mods = ImmutableList.builder(); + int modCount = ProtocolUtils.readVarInt(byteBuf); + + for (int index = 0; index < modCount; index++) { + String id = ProtocolUtils.readString(byteBuf); + String version = ProtocolUtils.readString(byteBuf); + mods.add(new ModInfo.Mod(id, version)); + } + + return Optional.of(mods.build()); + } + + return Optional.empty(); + } finally { + byteBuf.release(); + } + } }