From 4764136d47f63d201914c3359430cf4875356434 Mon Sep 17 00:00:00 2001 From: JoJoDeveloping Date: Mon, 18 Mar 2019 00:01:23 +0100 Subject: [PATCH] Re-Add the list ping compatibility checker Signed-off-by: JoJoDeveloping --- .../gui/ServerListEntryNormal.java.patch | 10 + .../client/multiplayer/ServerData.java.patch | 10 + .../client/network/ServerPinger.java.patch | 11 + .../network/ServerStatusResponse.java.patch | 45 +++- .../net/minecraftforge/fml/ModLoader.java | 2 + .../fml/client/ClientHooks.java | 192 +++++++----------- .../fml/client/ExtendedServerListData.java | 15 +- .../fml/network/FMLStatusPing.java | 123 +++++++++++ .../fml/network/NetworkRegistry.java | 77 ++++++- .../fml/server/ServerModLoader.java | 2 + .../registries/RegistryManager.java | 4 + .../resources/assets/forge/lang/en_us.json | 8 + .../assets/forge/textures/gui/icons.png | Bin 5339 -> 10289 bytes 13 files changed, 365 insertions(+), 134 deletions(-) create mode 100644 patches/minecraft/net/minecraft/client/gui/ServerListEntryNormal.java.patch create mode 100644 patches/minecraft/net/minecraft/client/multiplayer/ServerData.java.patch create mode 100644 patches/minecraft/net/minecraft/client/network/ServerPinger.java.patch create mode 100644 src/main/java/net/minecraftforge/fml/network/FMLStatusPing.java 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 000000000..a6d90336f --- /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 000000000..69f93e02a --- /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 000000000..9687fa241 --- /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 834038b17..ddc1d9bdc 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 ca5f5cfc4..21170287c 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 da8f7d981..9e12744ef 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 3befb715f..51c4ce988 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 000000000..b51df8311 --- /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 5e7f8e34e..51cd1f70d 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 2282639bc..1304cd04f 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 3c573aa48..9c068c3d6 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 a30ccd770..01db13f28 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 01ae2e62bc38a21f956029d005854cfd70525676..846844a7b72f2a29804675e00f73b445a6740244 100644 GIT binary patch literal 10289 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�wKKd-5^zWJi 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 URVlvAOZov|IaS#zY2(oU11_CEZvX%Q literal 5339 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