diff --git a/patches/minecraft/net/minecraft/client/gui/ServerListEntryNormal.java.patch b/patches/minecraft/net/minecraft/client/gui/ServerListEntryNormal.java.patch new file mode 100644 index 0000000000..a6d90336f2 --- /dev/null +++ b/patches/minecraft/net/minecraft/client/gui/ServerListEntryNormal.java.patch @@ -0,0 +1,10 @@ +--- a/net/minecraft/client/gui/ServerListEntryNormal.java ++++ b/net/minecraft/client/gui/ServerListEntryNormal.java +@@ -143,6 +143,7 @@ + } else if (k1 >= p_194999_1_ - l - 15 - 2 && k1 <= p_194999_1_ - 15 - 2 && l1 >= 0 && l1 <= 8) { + this.field_148303_c.func_146793_a(s); + } ++ net.minecraftforge.fml.client.ClientHooks.drawForgePingInfo(this.field_148303_c, field_148301_e, j, i, p_194999_1_, k1, l1); + + if (this.field_148300_d.field_71474_y.field_85185_A || p_194999_5_) { + this.field_148300_d.func_110434_K().func_110577_a(field_178014_d); diff --git a/patches/minecraft/net/minecraft/client/multiplayer/ServerData.java.patch b/patches/minecraft/net/minecraft/client/multiplayer/ServerData.java.patch new file mode 100644 index 0000000000..69f93e02ac --- /dev/null +++ b/patches/minecraft/net/minecraft/client/multiplayer/ServerData.java.patch @@ -0,0 +1,10 @@ +--- a/net/minecraft/client/multiplayer/ServerData.java ++++ b/net/minecraft/client/multiplayer/ServerData.java +@@ -20,6 +20,7 @@ + private ServerData.ServerResourceMode field_152587_j = ServerData.ServerResourceMode.PROMPT; + private String field_147411_m; + private boolean field_181042_l; ++ public net.minecraftforge.fml.client.ExtendedServerListData forgeData = null; + + public ServerData(String p_i46420_1_, String p_i46420_2_, boolean p_i46420_3_) { + this.field_78847_a = p_i46420_1_; diff --git a/patches/minecraft/net/minecraft/client/network/ServerPinger.java.patch b/patches/minecraft/net/minecraft/client/network/ServerPinger.java.patch new file mode 100644 index 0000000000..9687fa2417 --- /dev/null +++ b/patches/minecraft/net/minecraft/client/network/ServerPinger.java.patch @@ -0,0 +1,11 @@ +--- a/net/minecraft/client/network/ServerPinger.java ++++ b/net/minecraft/client/network/ServerPinger.java +@@ -120,6 +120,8 @@ + p_147224_1_.func_147407_a((String)null); + } + ++ net.minecraftforge.fml.client.ClientHooks.processForgeListPingData(serverstatusresponse, p_147224_1_); ++ + this.field_175092_e = Util.func_211177_b(); + networkmanager.func_179290_a(new CPacketPing(this.field_175092_e)); + this.field_147403_d = true; diff --git a/patches/minecraft/net/minecraft/network/ServerStatusResponse.java.patch b/patches/minecraft/net/minecraft/network/ServerStatusResponse.java.patch index 834038b175..ddc1d9bdc7 100644 --- a/patches/minecraft/net/minecraft/network/ServerStatusResponse.java.patch +++ b/patches/minecraft/net/minecraft/network/ServerStatusResponse.java.patch @@ -1,6 +1,23 @@ --- a/net/minecraft/network/ServerStatusResponse.java +++ b/net/minecraft/network/ServerStatusResponse.java -@@ -26,6 +26,7 @@ +@@ -19,13 +19,24 @@ + private ServerStatusResponse.Players field_151324_b; + private ServerStatusResponse.Version field_151325_c; + private String field_151323_d; ++ private net.minecraftforge.fml.network.FMLStatusPing forgeData; + ++ public net.minecraftforge.fml.network.FMLStatusPing getForgeData() { ++ return this.forgeData; ++ } ++ ++ public void setForgeData(net.minecraftforge.fml.network.FMLStatusPing data){ ++ this.forgeData = data; ++ invalidateJson(); ++ } ++ + public ITextComponent func_151317_a() { + return this.field_151326_a; + } public void func_151315_a(ITextComponent p_151315_1_) { this.field_151326_a = p_151315_1_; @@ -8,7 +25,7 @@ } public ServerStatusResponse.Players func_151318_b() { -@@ -34,6 +35,7 @@ +@@ -34,6 +45,7 @@ public void func_151319_a(ServerStatusResponse.Players p_151319_1_) { this.field_151324_b = p_151319_1_; @@ -16,7 +33,7 @@ } public ServerStatusResponse.Version func_151322_c() { -@@ -42,16 +44,51 @@ +@@ -42,16 +54,51 @@ public void func_151321_a(ServerStatusResponse.Version p_151321_1_) { this.field_151325_c = p_151321_1_; @@ -68,3 +85,25 @@ public static class Players { private final int field_151336_a; private final int field_151334_b; +@@ -143,6 +190,10 @@ + serverstatusresponse.func_151320_a(JsonUtils.func_151200_h(jsonobject, "favicon")); + } + ++ if (jsonobject.has("forgeData")) { ++ serverstatusresponse.setForgeData(net.minecraftforge.fml.network.FMLStatusPing.Serializer.deserialize(JsonUtils.func_152754_s(jsonobject, "forgeData"), p_deserialize_3_)); ++ } ++ + return serverstatusresponse; + } + +@@ -164,6 +215,10 @@ + jsonobject.addProperty("favicon", p_serialize_1_.func_151316_d()); + } + ++ if(p_serialize_1_.getForgeData() != null){ ++ jsonobject.add("forgeData", net.minecraftforge.fml.network.FMLStatusPing.Serializer.serialize(p_serialize_1_.getForgeData(), p_serialize_3_)); ++ } ++ + return jsonobject; + } + } diff --git a/src/main/java/net/minecraftforge/fml/ModLoader.java b/src/main/java/net/minecraftforge/fml/ModLoader.java index ca5f5cfc42..21170287cc 100644 --- a/src/main/java/net/minecraftforge/fml/ModLoader.java +++ b/src/main/java/net/minecraftforge/fml/ModLoader.java @@ -33,6 +33,7 @@ import net.minecraftforge.fml.loading.LoadingModList; import net.minecraftforge.fml.loading.moddiscovery.ModFile; import net.minecraftforge.fml.loading.moddiscovery.ModFileInfo; import net.minecraftforge.fml.network.FMLNetworkConstants; +import net.minecraftforge.fml.network.NetworkRegistry; import net.minecraftforge.forgespi.language.IModInfo; import net.minecraftforge.forgespi.language.IModLanguageProvider; import net.minecraftforge.registries.GameData; @@ -190,6 +191,7 @@ public class ModLoader dispatchAndHandleError(LifecycleEventProvider.PROCESS_IMC); dispatchAndHandleError(LifecycleEventProvider.COMPLETE); GameData.freezeData(); + NetworkRegistry.lock(); } public List getWarnings() diff --git a/src/main/java/net/minecraftforge/fml/client/ClientHooks.java b/src/main/java/net/minecraftforge/fml/client/ClientHooks.java index da8f7d9818..9e12744ef3 100644 --- a/src/main/java/net/minecraftforge/fml/client/ClientHooks.java +++ b/src/main/java/net/minecraftforge/fml/client/ClientHooks.java @@ -20,16 +20,20 @@ package net.minecraftforge.fml.client; import java.io.File; -import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.concurrent.CountDownLatch; import java.util.stream.Collectors; import javax.annotation.Nullable; +import com.google.common.collect.*; +import net.minecraft.client.gui.*; +import net.minecraftforge.fml.ForgeI18n; +import net.minecraftforge.fml.network.FMLNetworkConstants; +import net.minecraftforge.fml.network.NetworkRegistry; +import net.minecraftforge.registries.RegistryManager; +import net.minecraftforge.versions.forge.ForgeVersion; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Marker; @@ -38,19 +42,8 @@ import org.apache.logging.log4j.MarkerManager; import com.google.common.base.CharMatcher; import com.google.common.base.MoreObjects; import com.google.common.base.Strings; -import com.google.common.collect.HashBasedTable; -import com.google.common.collect.HashMultimap; -import com.google.common.collect.SetMultimap; -import com.google.common.collect.Sets; -import com.google.common.collect.Table; -import com.google.gson.JsonObject; import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.Gui; -import net.minecraft.client.gui.GuiConnecting; -import net.minecraft.client.gui.GuiScreen; -import net.minecraft.client.gui.GuiWorldSelection; -import net.minecraft.client.gui.ServerListEntryNormal; import net.minecraft.client.multiplayer.ServerData; import net.minecraft.client.multiplayer.WorldClient; import net.minecraft.network.NetworkManager; @@ -62,7 +55,6 @@ import net.minecraft.resources.SimpleReloadableResourceManager; import net.minecraft.util.ResourceLocation; import net.minecraft.world.storage.WorldSummary; import net.minecraftforge.fml.StartupQuery; -import net.minecraftforge.fml.client.gui.GuiAccessDenied; import net.minecraftforge.fml.common.ObfuscationReflectionHelper; import net.minecraftforge.forgespi.language.IModInfo; import net.minecraftforge.fml.packs.ModFileResourcePack; @@ -76,108 +68,78 @@ public class ClientHooks private static final String ALLOWED_CHARS = "\u00c0\u00c1\u00c2\u00c8\u00ca\u00cb\u00cd\u00d3\u00d4\u00d5\u00da\u00df\u00e3\u00f5\u011f\u0130\u0131\u0152\u0153\u015e\u015f\u0174\u0175\u017e\u0207\u0000\u0000\u0000\u0000\u0000\u0000\u0000 !\"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\u0000\u00c7\u00fc\u00e9\u00e2\u00e4\u00e0\u00e5\u00e7\u00ea\u00eb\u00e8\u00ef\u00ee\u00ec\u00c4\u00c5\u00c9\u00e6\u00c6\u00f4\u00f6\u00f2\u00fb\u00f9\u00ff\u00d6\u00dc\u00f8\u00a3\u00d8\u00d7\u0192\u00e1\u00ed\u00f3\u00fa\u00f1\u00d1\u00aa\u00ba\u00bf\u00ae\u00ac\u00bd\u00bc\u00a1\u00ab\u00bb\u2591\u2592\u2593\u2502\u2524\u2561\u2562\u2556\u2555\u2563\u2551\u2557\u255d\u255c\u255b\u2510\u2514\u2534\u252c\u251c\u2500\u253c\u255e\u255f\u255a\u2554\u2569\u2566\u2560\u2550\u256c\u2567\u2568\u2564\u2565\u2559\u2558\u2552\u2553\u256b\u256a\u2518\u250c\u2588\u2584\u258c\u2590\u2580\u03b1\u03b2\u0393\u03c0\u03a3\u03c3\u03bc\u03c4\u03a6\u0398\u03a9\u03b4\u221e\u2205\u2208\u2229\u2261\u00b1\u2265\u2264\u2320\u2321\u00f7\u2248\u00b0\u2219\u00b7\u221a\u207f\u00b2\u25a0\u0000"; private static final CharMatcher DISALLOWED_CHAR_MATCHER = CharMatcher.anyOf(ALLOWED_CHARS).negate(); - private static Map extraServerListData; - private static Map serverDataTag; - - public static void setupServerList() - { - extraServerListData = Collections.synchronizedMap(new HashMap<>()); - serverDataTag = Collections.synchronizedMap(new HashMap<>()); - } - - public static void captureAdditionalData(ServerStatusResponse serverstatusresponse, JsonObject jsonobject) - { - if (jsonobject.has("modinfo")) - { - JsonObject fmlData = jsonobject.get("modinfo").getAsJsonObject(); - extraServerListData.put(serverstatusresponse, fmlData); - } - } - public static void bindServerListData(ServerData data, ServerStatusResponse originalResponse) - { -/* - if (extraServerListData.containsKey(originalResponse)) - { - JsonObject jsonData = extraServerListData.get(originalResponse); - String type = jsonData.get("type").getAsString(); - JsonArray modDataArray = jsonData.get("modList").getAsJsonArray(); - boolean moddedClientAllowed = !jsonData.has("clientModsAllowed") || jsonData.get("clientModsAllowed").getAsBoolean(); - ImmutableMap.Builder modListBldr = ImmutableMap.builder(); - for (JsonElement obj : modDataArray) - { - JsonObject modObj = obj.getAsJsonObject(); - modListBldr.put(modObj.get("modid").getAsString(), modObj.get("version").getAsString()); - } - - Map modListMap = modListBldr.build(); - String modRejections = FMLNetworkHandler.checkModList(modListMap, LogicalSide.SERVER); - serverDataTag.put(data, new ExtendedServerListData(type, modRejections == null, modListMap, !moddedClientAllowed)); - } - else - { - String serverDescription = data.serverMOTD; - boolean moddedClientAllowed = true; - if (!Strings.isNullOrEmpty(serverDescription)) - { - moddedClientAllowed = !serverDescription.endsWith(":NOFML§r"); - } - serverDataTag.put(data, new ExtendedServerListData("VANILLA", false, ImmutableMap.of(), !moddedClientAllowed)); - } -*/ - startupConnectionData.countDown(); - } - - private static final ResourceLocation iconSheet = new ResourceLocation("fml:textures/gui/icons.png"); - private static final CountDownLatch startupConnectionData = new CountDownLatch(1); - + private static final ResourceLocation iconSheet = new ResourceLocation(ForgeVersion.MOD_ID, "textures/gui/icons.png"); @Nullable - public static String enhanceServerListEntry(ServerListEntryNormal serverListEntry, ServerData serverEntry, int x, int width, int y, int relativeMouseX, int relativeMouseY) + public static void processForgeListPingData(ServerStatusResponse packet, ServerData target) { - String tooltip; + if(packet.getForgeData() != null){ + int numberOfMods = packet.getForgeData().getNumberOfMods(); + MapDifference difference = Maps.difference(packet.getForgeData().getRegistryHashes(), RegistryManager.ACTIVE.computeRegistryHashes()); + int fmlver = packet.getForgeData().getFMLNetworkVersion(); + + boolean b = NetworkRegistry.checkListPingCompatibilityForClient(packet.getForgeData().getPresentMods()) + && difference.areEqual() + && fmlver == FMLNetworkConstants.FMLNETVERSION; + + LOGGER.debug(CLIENTHOOKS, "Received FML ping data from server at {}: FMLNETVER={}, {} mods, channels: [{}] - compatible: {}", target.serverIP, fmlver, numberOfMods, packet.getForgeData().getPresentMods().entrySet(), b); + difference.entriesDiffering().forEach((k,vd)-> LOGGER.debug(CLIENTHOOKS, "Registry {}: Local: {}, Remote: {}", k, vd.rightValue(), vd.leftValue())); + difference.entriesOnlyOnLeft().forEach((k,vd)-> LOGGER.debug(CLIENTHOOKS, "Registry {} is only on server with hash {}", k, vd)); + difference.entriesOnlyOnRight().forEach((k,vd)-> LOGGER.debug(CLIENTHOOKS, "Registry {} is missing on server with hash {}", k, vd)); + difference.entriesInCommon().forEach((k,vd)-> LOGGER.debug(CLIENTHOOKS, "Registry {} is equal, hash={}", k, vd)); + + String extraReason = null; + if(fmlver FMLNetworkConstants.FMLNETVERSION) + extraReason = "fml.menu.multiplayer.clientoutdated"; + + target.forgeData = new ExtendedServerListData("FML", b, packet.getForgeData().getPresentMods(), numberOfMods, extraReason); + }else{ + target.forgeData = new ExtendedServerListData("VANILLA", NetworkRegistry.canConnectToVanillaServer(), Maps.newHashMap(), 0, null); + } + + } + + public static void drawForgePingInfo(GuiMultiplayer gui, ServerData target, int x, int y, int width, int relativeMouseX, int relativeMouseY){ int idx; - boolean blocked = false; - if (serverDataTag.containsKey(serverEntry)) - { - ExtendedServerListData extendedData = serverDataTag.get(serverEntry); - if ("FML".equals(extendedData.type) && extendedData.isCompatible) - { - idx = 0; - tooltip = String.format("Compatible FML modded server\n%d mods present", extendedData.modData.size()); - } - else if ("FML".equals(extendedData.type) && !extendedData.isCompatible) - { - idx = 16; - tooltip = String.format("Incompatible FML modded server\n%d mods present", extendedData.modData.size()); - } - else if ("BUKKIT".equals(extendedData.type)) - { - idx = 32; - tooltip = String.format("Bukkit modded server"); - } - else if ("VANILLA".equals(extendedData.type)) - { - idx = 48; - tooltip = String.format("Vanilla server"); - } - else - { + String tooltip; + if(target.forgeData == null) + return; + switch (target.forgeData.type){ + case "FML": + if (target.forgeData.isCompatible) { + idx = 0; + tooltip = ForgeI18n.parseMessage("fml.menu.multiplayer.compatible", target.forgeData.numberOfMods); + } else { + idx = 16; + if(target.forgeData.extraReason != null) { + String extraReason = ForgeI18n.parseMessage(target.forgeData.extraReason); + tooltip = ForgeI18n.parseMessage("fml.menu.multiplayer.incompatible.extra", extraReason); + } else { + tooltip = ForgeI18n.parseMessage("fml.menu.multiplayer.incompatible"); + } + } + break; + case "VANILLA": + if(target.forgeData.isCompatible) { + idx = 48; + tooltip = ForgeI18n.parseMessage("fml.menu.multiplayer.vanilla"); + } else { + idx = 80; + tooltip = ForgeI18n.parseMessage("fml.menu.multiplayer.vanilla.incompatible"); + } + break; + default: idx = 64; - tooltip = String.format("Unknown server data"); - } - blocked = extendedData.isBlocked; - } - else - { - return null; + tooltip = ForgeI18n.parseMessage("fml.menu.multiplayer.unknown", target.forgeData.type); } + Minecraft.getInstance().getTextureManager().bindTexture(iconSheet); Gui.drawModalRectWithCustomSizedTexture(x + width - 18, y + 10, 0, (float)idx, 16, 16, 256.0f, 256.0f); - if (blocked) - { - Gui.drawModalRectWithCustomSizedTexture(x + width - 18, y + 10, 0, 80, 16, 16, 256.0f, 256.0f); - } - return relativeMouseX > width - 15 && relativeMouseX < width && relativeMouseY > 10 && relativeMouseY < 26 ? tooltip : null; + if(relativeMouseX > width - 15 && relativeMouseX < width && relativeMouseY > 10 && relativeMouseY < 26) + gui.setHoveringText(tooltip); + } public static String fixDescription(String description) @@ -217,18 +179,6 @@ public class ClientHooks } } - public static void connectToServer(GuiScreen guiMultiplayer, ServerData serverEntry) - { - ExtendedServerListData extendedData = serverDataTag.get(serverEntry); - if (extendedData != null && extendedData.isBlocked) - { - Minecraft.getInstance().displayGuiScreen(new GuiAccessDenied(guiMultiplayer, serverEntry)); - } - else - { - Minecraft.getInstance().displayGuiScreen(new GuiConnecting(guiMultiplayer, Minecraft.getInstance(), serverEntry)); - } - } public static String stripSpecialChars(String message) { diff --git a/src/main/java/net/minecraftforge/fml/client/ExtendedServerListData.java b/src/main/java/net/minecraftforge/fml/client/ExtendedServerListData.java index 3befb715fc..51c4ce9888 100644 --- a/src/main/java/net/minecraftforge/fml/client/ExtendedServerListData.java +++ b/src/main/java/net/minecraftforge/fml/client/ExtendedServerListData.java @@ -19,19 +19,24 @@ package net.minecraftforge.fml.client; +import net.minecraft.util.ResourceLocation; +import org.apache.commons.lang3.tuple.Pair; + import java.util.Map; public class ExtendedServerListData { public final String type; public final boolean isCompatible; - public final Map modData; - public final boolean isBlocked; + public final Map> channelData; + public int numberOfMods; + public String extraReason; - public ExtendedServerListData(String type, boolean isCompatible, Map modData, boolean isBlocked) + public ExtendedServerListData(String type, boolean isCompatible, Map> channelData, int num, String extraReason) { this.type = type; this.isCompatible = isCompatible; - this.modData = modData; - this.isBlocked = isBlocked; + this.channelData = channelData; + this.numberOfMods = num; + this.extraReason = extraReason; } } diff --git a/src/main/java/net/minecraftforge/fml/network/FMLStatusPing.java b/src/main/java/net/minecraftforge/fml/network/FMLStatusPing.java new file mode 100644 index 0000000000..b51df83112 --- /dev/null +++ b/src/main/java/net/minecraftforge/fml/network/FMLStatusPing.java @@ -0,0 +1,123 @@ +/* + * Minecraft Forge + * Copyright (c) 2016-2019. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation version 2.1 + * of the License. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package net.minecraftforge.fml.network; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; +import com.google.gson.*; +import net.minecraft.util.JsonUtils; +import net.minecraft.util.ResourceLocation; +import net.minecraftforge.fml.ModList; +import net.minecraftforge.registries.RegistryManager; +import org.apache.commons.lang3.tuple.Pair; + +import java.util.Map; + +public class FMLStatusPing { + + private Map> channelVersions; + private int numberOfMods; + private int fmlNetworkVer; + private Map registrySnapshots; + + public FMLStatusPing(){ + this.channelVersions = NetworkRegistry.buildChannelVersionsForListPing(); + this.numberOfMods = ModList.get().size(); + this.registrySnapshots = RegistryManager.ACTIVE.computeRegistryHashes(); + this.fmlNetworkVer = FMLNetworkConstants.FMLNETVERSION; + } + + private FMLStatusPing(Map> deserialized, Map registryHashes, int nom, int fmlNetVer){ + this.channelVersions = ImmutableMap.copyOf(deserialized); + this.numberOfMods = nom; + this.registrySnapshots = registryHashes; + this.fmlNetworkVer = fmlNetVer; + } + + public static class Serializer { + + public static FMLStatusPing deserialize(JsonObject forgeData, JsonDeserializationContext ctx) { + try { + JsonArray mods = JsonUtils.getJsonArray(forgeData, "mods"); + Map> versions = Maps.newHashMap(); + for(JsonElement el : mods){ + JsonObject jo = el.getAsJsonObject(); + ResourceLocation name = new ResourceLocation(JsonUtils.getString(jo, "namespace"), JsonUtils.getString(jo, "path")); + String version = JsonUtils.getString(jo, "version"); + Boolean canBeAbsent = JsonUtils.getBoolean(jo, "mayBeAbsent"); + versions.put(name, Pair.of(version, canBeAbsent)); + } + JsonArray reghashes = JsonUtils.getJsonArray(forgeData, "registryKeys"); + Map registyData = Maps.newHashMap(); + for(JsonElement el : reghashes){ + JsonObject jo = el.getAsJsonObject(); + ResourceLocation name = new ResourceLocation(JsonUtils.getString(jo, "namespace"), JsonUtils.getString(jo, "path")); + registyData.put(name, JsonUtils.getInt(jo, "hash")); + } + return new FMLStatusPing(versions, registyData, JsonUtils.getInt(forgeData, "numberOfMods"), JsonUtils.getInt(forgeData, "fmlNetworkVersion")); + }catch (Exception c){ + return null; + } + } + + public static JsonObject serialize(FMLStatusPing forgeData, JsonSerializationContext ctx){ + JsonObject obj = new JsonObject(); + JsonArray mods = new JsonArray(); + forgeData.channelVersions.entrySet().stream().map(p -> { + JsonObject mi = new JsonObject(); + mi.addProperty("namespace", p.getKey().getNamespace()); + mi.addProperty("path", p.getKey().getPath()); + mi.addProperty("version", p.getValue().getKey()); + mi.addProperty("mayBeAbsent", p.getValue().getValue()); + return mi; + }).forEach(mods::add); + obj.add("mods", mods); + JsonArray regdata = new JsonArray(); + forgeData.registrySnapshots.entrySet().stream().map(p -> { + JsonObject mi = new JsonObject(); + mi.addProperty("namespace", p.getKey().getNamespace()); + mi.addProperty("path", p.getKey().getPath()); + mi.addProperty("hash", p.getValue()); + return mi; + }).forEach(regdata::add); + obj.add("registryKeys", regdata); + obj.addProperty("numberOfMods", forgeData.numberOfMods); + obj.addProperty("fmlNetworkVersion", forgeData.fmlNetworkVer); + return obj; + } + } + + public Map> getPresentMods(){ + return this.channelVersions; + } + + public int getNumberOfMods(){ + return numberOfMods; + } + + public Map getRegistryHashes(){ + return registrySnapshots; + } + + public int getFMLNetworkVersion(){ + return fmlNetworkVer; + } + +} diff --git a/src/main/java/net/minecraftforge/fml/network/NetworkRegistry.java b/src/main/java/net/minecraftforge/fml/network/NetworkRegistry.java index 5e7f8e34e6..51cd1f70d7 100644 --- a/src/main/java/net/minecraftforge/fml/network/NetworkRegistry.java +++ b/src/main/java/net/minecraftforge/fml/network/NetworkRegistry.java @@ -19,8 +19,6 @@ package net.minecraftforge.fml.network; -import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.nbt.NBTTagList; import net.minecraft.network.NetworkManager; import net.minecraft.network.PacketBuffer; import net.minecraft.util.ResourceLocation; @@ -38,6 +36,8 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; +import java.util.HashSet; import java.util.function.BiFunction; import java.util.function.Predicate; import java.util.function.Supplier; @@ -63,13 +63,22 @@ public class NetworkRegistry @SuppressWarnings("RedundantStringConstructorCall") public static String ACCEPTVANILLA = new String("ALLOWVANILLA \uD83D\uDC93\uD83D\uDC93\uD83D\uDC93"); - public static List getNonVanillaNetworkMods() + public static List getServerNonVanillaNetworkMods() { return listRejectedVanillaMods(NetworkInstance::tryClientVersionOnServer); } + public static List getClientNonVanillaNetworkMods() + { + return listRejectedVanillaMods(NetworkInstance::tryServerVersionOnClient); + } + public static boolean acceptsVanillaClientConnections() { - return instances.isEmpty() || getNonVanillaNetworkMods().isEmpty(); + return instances.isEmpty() || getServerNonVanillaNetworkMods().isEmpty(); + } + + public static boolean canConnectToVanillaServer() { + return instances.isEmpty() || getClientNonVanillaNetworkMods().isEmpty(); } @@ -116,6 +125,10 @@ public class NetworkRegistry */ private static NetworkInstance createInstance(ResourceLocation name, Supplier networkProtocolVersion, Predicate clientAcceptedVersions, Predicate serverAcceptedVersions) { + if(lock && !name.getNamespace().equals("fml")){ + LOGGER.error(NETREGISTRY, "Attempted to register channel {} even though registry phase is over", name); + throw new IllegalArgumentException("Registration of network channels is locked"); + } final NetworkInstance networkInstance = new NetworkInstance(name, networkProtocolVersion, clientAcceptedVersions, serverAcceptedVersions); if (instances.containsKey(name)) { LOGGER.error(NETREGISTRY, "NetworkDirection channel {} already registered.", name); @@ -146,6 +159,19 @@ public class NetworkRegistry return instances.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().getNetworkProtocolVersion())); } + /** + * Construct the Map representation of the channel list, for the client to check against during list ping + * + * @see FMLHandshakeMessages.S2CModList + * @see FMLHandshakeMessages.C2SModListReply + */ + static Map> buildChannelVersionsForListPing() { + return instances.entrySet().stream(). + map( p -> Pair.of(p.getKey(), Pair.of(p.getValue().getNetworkProtocolVersion(), p.getValue().tryClientVersionOnServer(ABSENT)))). + filter(p -> !p.getLeft().getNamespace().equals("fml")). + collect(Collectors.toMap(Pair::getLeft, Pair::getRight)); + } + static List listRejectedVanillaMods(BiFunction testFunction) { final List> results = instances.values().stream(). map(ni -> { @@ -187,7 +213,7 @@ public class NetworkRegistry /** * Tests if the map matches with the supplied predicate tester * - * @param channels An @{@link Map} of name->version pairs for testing + * @param incoming An @{@link Map} of name->version pairs for testing * @param originName A label for use in logging (where the version pairs came from) * @param testFunction The test function to use for testing * @return true if all channels accept themselves @@ -225,6 +251,47 @@ public class NetworkRegistry return gatheredPayloads; } + public static boolean checkListPingCompatibilityForClient(Map> incoming) { + Set handled = new HashSet<>(); + final List> results = instances.values().stream(). + filter(p -> !p.getChannelName().getNamespace().equals("fml")). + map(ni -> { + final Pair incomingVersion = incoming.getOrDefault(ni.getChannelName(), Pair.of(ABSENT, true)); + final boolean test = ni.tryServerVersionOnClient(incomingVersion.getLeft()); + handled.add(ni.getChannelName()); + LOGGER.debug(NETREGISTRY, "Channel '{}' : Version test of '{}' during listping : {}", ni.getChannelName(), incomingVersion, test ? "ACCEPTED" : "REJECTED"); + return Pair.of(ni.getChannelName(), test); + }).filter(p->!p.getRight()).collect(Collectors.toList()); + final List missingButRequired = incoming.entrySet().stream(). + filter(p -> !p.getKey().getNamespace().equals("fml")). + filter(p -> !p.getValue().getRight()). + filter(p -> !handled.contains(p.getKey())). + map(Map.Entry::getKey). + collect(Collectors.toList()); + + if (!results.isEmpty()) { + LOGGER.error(NETREGISTRY, "Channels [{}] rejected their server side version number during listping", + results.stream().map(Pair::getLeft).map(Object::toString).collect(Collectors.joining(","))); + return false; + } + if(!missingButRequired.isEmpty()){ + LOGGER.error(NETREGISTRY, "The server is likely to require channel [{}] to be present, yet we don't have it", + missingButRequired); + return false; + } + LOGGER.debug(NETREGISTRY, "Accepting channel list during listping"); + return true; + } + + private static boolean lock = false; + public boolean isLocked(){ + return lock; + } + + public static void lock() { + lock=true; + } + /** * Tracks individual outbound messages for dispatch to clients during login handling. Gathered by dispatching * {@link net.minecraftforge.fml.network.NetworkEvent.GatherLoginPayloadsEvent} during early connection handling. diff --git a/src/main/java/net/minecraftforge/fml/server/ServerModLoader.java b/src/main/java/net/minecraftforge/fml/server/ServerModLoader.java index 2282639bc6..1304cd04fb 100644 --- a/src/main/java/net/minecraftforge/fml/server/ServerModLoader.java +++ b/src/main/java/net/minecraftforge/fml/server/ServerModLoader.java @@ -24,6 +24,7 @@ import net.minecraftforge.fml.LogicalSidedProvider; import net.minecraftforge.fml.ModLoader; import net.minecraftforge.fml.ModLoadingWarning; import net.minecraftforge.fml.SidedProvider; +import net.minecraftforge.fml.network.FMLStatusPing; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -50,5 +51,6 @@ public class ServerModLoader LOGGER.warn(LOADING, "Mods loaded with {} warnings", warnings.size()); warnings.forEach(warning -> LOGGER.warn(LOADING, warning.formatToString())); } + server.getServerStatusResponse().setForgeData(new FMLStatusPing()); //gathers NetworkRegistry data } } diff --git a/src/main/java/net/minecraftforge/registries/RegistryManager.java b/src/main/java/net/minecraftforge/registries/RegistryManager.java index 3c573aa484..9c068c3d69 100644 --- a/src/main/java/net/minecraftforge/registries/RegistryManager.java +++ b/src/main/java/net/minecraftforge/registries/RegistryManager.java @@ -159,4 +159,8 @@ public class RegistryManager { return new ArrayList<>(ACTIVE.registries.keySet()); } + + public Map computeRegistryHashes() { + return this.registries.entrySet().stream().map(p -> Pair.of(p.getKey(), p.getValue().getKeys().hashCode())).collect(Collectors.toMap(Pair::getLeft, Pair::getRight)); + } } diff --git a/src/main/resources/assets/forge/lang/en_us.json b/src/main/resources/assets/forge/lang/en_us.json index a30ccd770a..01db13f280 100644 --- a/src/main/resources/assets/forge/lang/en_us.json +++ b/src/main/resources/assets/forge/lang/en_us.json @@ -15,6 +15,14 @@ "fml.menu.mods.info.childmods":"Child mods: {0}", "fml.menu.mods.info.updateavailable":"Update available: {0}", "fml.menu.mods.info.changelogheader":"Changelog:", + "fml.menu.multiplayer.compatible":"Compatible FML modded server, {0,choice,1#1 mod|1<{0} mods} present", + "fml.menu.multiplayer.incompatible":"Incompatible FML modded server", + "fml.menu.multiplayer.incompatible.extra":"Incompatible FML modded server - {}", + "fml.menu.multiplayer.vanilla":"Vanilla server", + "fml.menu.multiplayer.vanilla.incompatible":"Incompatible Vanilla server", + "fml.menu.multiplayer.unknown":"Unknown server {0}", + "fml.menu.multiplayer.serveroutdated":"Outdated server", + "fml.menu.multiplayer.clientoutdated":"Outdated client", "fml.menu.loadingmods": "{0,choice,0#No mods|1#1 mod|1<{0} mods} loaded", "fml.button.open.file": "Open {0}", "fml.button.open.mods.folder": "Open Mods Folder", diff --git a/src/main/resources/assets/forge/textures/gui/icons.png b/src/main/resources/assets/forge/textures/gui/icons.png index 01ae2e62bc..846844a7b7 100644 Binary files a/src/main/resources/assets/forge/textures/gui/icons.png and b/src/main/resources/assets/forge/textures/gui/icons.png differ