From 3f735b715bc1ba19851ba8b02e1083cf8d328e8e Mon Sep 17 00:00:00 2001 From: cpw Date: Sun, 31 Mar 2019 16:36:49 -0400 Subject: [PATCH] Implement API for mods to control their server status response.. Signed-off-by: cpw --- .../network/ServerStatusResponse.java.patch | 2 +- .../net/minecraftforge/common/ForgeMod.java | 4 + .../minecraftforge/fml/ExtensionPoint.java | 13 +- .../net/minecraftforge/fml/ModContainer.java | 13 +- .../java/net/minecraftforge/fml/ModList.java | 4 + .../fml/client/ClientHooks.java | 73 +++++++--- .../fml/client/ExtendedServerListData.java | 9 +- .../fml/network/FMLNetworkConstants.java | 4 + .../fml/network/FMLStatusPing.java | 126 ++++++++++++------ .../resources/assets/forge/lang/en_us.json | 11 +- 10 files changed, 183 insertions(+), 76 deletions(-) diff --git a/patches/minecraft/net/minecraft/network/ServerStatusResponse.java.patch b/patches/minecraft/net/minecraft/network/ServerStatusResponse.java.patch index ddc1d9bdc..c6f512491 100644 --- a/patches/minecraft/net/minecraft/network/ServerStatusResponse.java.patch +++ b/patches/minecraft/net/minecraft/network/ServerStatusResponse.java.patch @@ -4,7 +4,7 @@ private ServerStatusResponse.Players field_151324_b; private ServerStatusResponse.Version field_151325_c; private String field_151323_d; -+ private net.minecraftforge.fml.network.FMLStatusPing forgeData; ++ private transient net.minecraftforge.fml.network.FMLStatusPing forgeData; + public net.minecraftforge.fml.network.FMLStatusPing getForgeData() { + return this.forgeData; diff --git a/src/main/java/net/minecraftforge/common/ForgeMod.java b/src/main/java/net/minecraftforge/common/ForgeMod.java index e633d3b37..6972abf2c 100644 --- a/src/main/java/net/minecraftforge/common/ForgeMod.java +++ b/src/main/java/net/minecraftforge/common/ForgeMod.java @@ -22,6 +22,7 @@ package net.minecraftforge.common; import net.minecraftforge.eventbus.api.IEventBus; import net.minecraftforge.fml.BrandingControl; import net.minecraftforge.fml.DistExecutor; +import net.minecraftforge.fml.ExtensionPoint; import net.minecraftforge.fml.FMLWorldPersistenceHook; import net.minecraftforge.fml.ModLoadingContext; import net.minecraftforge.fml.VersionChecker; @@ -38,6 +39,7 @@ import net.minecraftforge.server.command.ForgeCommand; import net.minecraftforge.versions.forge.ForgeVersion; import net.minecraftforge.versions.mcp.MCPVersion; +import org.apache.commons.lang3.tuple.Pair; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -99,6 +101,8 @@ public class ForgeMod implements WorldPersistenceHooks.WorldPersistenceHook ModLoadingContext.get().registerConfig(ModConfig.Type.CLIENT, ForgeConfig.clientSpec); ModLoadingContext.get().registerConfig(ModConfig.Type.SERVER, ForgeConfig.serverSpec); modEventBus.register(ForgeConfig.class); + // Forge does not display problems when the remote is not matching. + ModLoadingContext.get().registerExtensionPoint(ExtensionPoint.DISPLAYTEST, ()-> Pair.of(()->"ANY", (remote, isServer)-> true)); } /* diff --git a/src/main/java/net/minecraftforge/fml/ExtensionPoint.java b/src/main/java/net/minecraftforge/fml/ExtensionPoint.java index 9cce0bab1..b845e1523 100644 --- a/src/main/java/net/minecraftforge/fml/ExtensionPoint.java +++ b/src/main/java/net/minecraftforge/fml/ExtensionPoint.java @@ -24,17 +24,28 @@ import net.minecraft.client.gui.GuiScreen; import net.minecraft.resources.IResourcePack; import net.minecraftforge.fml.network.FMLPlayMessages; import net.minecraftforge.fml.packs.ModFileResourcePack; +import org.apache.commons.lang3.tuple.Pair; import java.util.function.BiFunction; +import java.util.function.BiPredicate; import java.util.function.Function; +import java.util.function.Supplier; public class ExtensionPoint { public static final ExtensionPoint> CONFIGGUIFACTORY = new ExtensionPoint<>(); public static final ExtensionPoint> RESOURCEPACK = new ExtensionPoint<>(); + /** + * Compatibility display test for the mod. + * Used for displaying compatibility with remote servers with the same mod, and on disk saves. + * + * The supplier provides my "local" version for sending across the network or writing to disk + * The predicate tests the version from a remote instance or save for acceptability (Boolean is true for network, false for local save) + */ + public static final ExtensionPoint, BiPredicate>> DISPLAYTEST = new ExtensionPoint<>(); /** - * Register with {@link ModLoadingContext#} + * Register with {@link ModLoadingContext#registerExtensionPoint(ExtensionPoint, Supplier)} */ public static final ExtensionPoint> GUIFACTORY = new ExtensionPoint<>(); diff --git a/src/main/java/net/minecraftforge/fml/ModContainer.java b/src/main/java/net/minecraftforge/fml/ModContainer.java index 684fea104..c1856b79f 100644 --- a/src/main/java/net/minecraftforge/fml/ModContainer.java +++ b/src/main/java/net/minecraftforge/fml/ModContainer.java @@ -21,8 +21,16 @@ package net.minecraftforge.fml; import net.minecraftforge.fml.config.ModConfig; import net.minecraftforge.forgespi.language.IModInfo; +import org.apache.commons.lang3.tuple.Pair; -import java.util.*; +import java.util.Collections; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; import java.util.function.Consumer; import java.util.function.Supplier; @@ -60,6 +68,9 @@ public abstract class ModContainer this.modInfo = info; this.triggerMap = new HashMap<>(); this.modLoadingStage = ModLoadingStage.CONSTRUCT; + // default displaytest extension checks for version string match + registerExtensionPoint(ExtensionPoint.DISPLAYTEST, () -> Pair.of(()->this.modInfo.getVersion().toString(), + (incoming, isNetwork)->Objects.equals(incoming, this.modInfo.getVersion().toString()))); } /** diff --git a/src/main/java/net/minecraftforge/fml/ModList.java b/src/main/java/net/minecraftforge/fml/ModList.java index 5039a7c59..7a0a4384f 100644 --- a/src/main/java/net/minecraftforge/fml/ModList.java +++ b/src/main/java/net/minecraftforge/fml/ModList.java @@ -183,4 +183,8 @@ public class ModList { modFiles.stream().map(ModFileInfo::getFile).forEach(fileConsumer); } + + public void forEachModContainer(BiConsumer modContainerConsumer) { + indexedMods.forEach(modContainerConsumer); + } } diff --git a/src/main/java/net/minecraftforge/fml/client/ClientHooks.java b/src/main/java/net/minecraftforge/fml/client/ClientHooks.java index 6abd292aa..147f7efe0 100644 --- a/src/main/java/net/minecraftforge/fml/client/ClientHooks.java +++ b/src/main/java/net/minecraftforge/fml/client/ClientHooks.java @@ -22,18 +22,28 @@ package net.minecraftforge.fml.client; import java.io.File; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; import javax.annotation.Nullable; -import com.google.common.collect.*; -import net.minecraft.client.gui.*; +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 net.minecraft.client.gui.Gui; +import net.minecraft.client.gui.GuiMultiplayer; +import net.minecraft.client.gui.GuiWorldSelection; +import net.minecraftforge.fml.ExtensionPoint; import net.minecraftforge.fml.ForgeI18n; +import net.minecraftforge.fml.ModList; 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.commons.lang3.tuple.Pair; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Marker; @@ -70,36 +80,59 @@ public class ClientHooks private static final ResourceLocation iconSheet = new ResourceLocation(ForgeVersion.MOD_ID, "textures/gui/icons.png"); @Nullable + public static void processForgeListPingData(ServerStatusResponse packet, ServerData target) { - if(packet.getForgeData() != null){ - int numberOfMods = packet.getForgeData().getNumberOfMods(); - int fmlver = packet.getForgeData().getFMLNetworkVersion(); + if (packet.getForgeData() != null) { + final Map mods = packet.getForgeData().getRemoteModData(); + final Map> remoteChannels = packet.getForgeData().getRemoteChannels(); + final int fmlver = packet.getForgeData().getFMLNetworkVersion(); - boolean b = NetworkRegistry.checkListPingCompatibilityForClient(packet.getForgeData().getPresentMods()) - && fmlver == FMLNetworkConstants.FMLNETVERSION; + boolean fmlNetMatches = fmlver == FMLNetworkConstants.FMLNETVERSION; + boolean channelsMatch = NetworkRegistry.checkListPingCompatibilityForClient(remoteChannels); + AtomicBoolean result = new AtomicBoolean(true); + ModList.get().forEachModContainer((modid, mc)-> mc.getCustomExtension(ExtensionPoint.DISPLAYTEST).ifPresent(ext-> + result.compareAndSet(true, ext.getRight().test(mods.get(modid), true)))); + boolean modsMatch = result.get(); - LOGGER.debug(CLIENTHOOKS, "Received FML ping data from server at {}: FMLNETVER={}, {} mods, channels: [{}] - compatible: {}", target.serverIP, fmlver, numberOfMods, packet.getForgeData().getPresentMods().entrySet(), b); + final Map extraServerMods = mods.entrySet().stream(). + filter(e -> !Objects.equals(FMLNetworkConstants.IGNORESERVERONLY, e.getValue())). + filter(e -> !ModList.get().isLoaded(e.getKey())). + collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + + LOGGER.debug(CLIENTHOOKS, "Received FML ping data from server at {}: FMLNETVER={}, mod list is compatible : {}, channel list is compatible: {}, extra server mods: {}", target.serverIP, fmlver, modsMatch, channelsMatch, extraServerMods); 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); + if (!extraServerMods.isEmpty()) { + extraReason = "fml.menu.multiplayer.extraservermods"; + } + if (!modsMatch) { + extraReason = "fml.menu.multiplayer.modsincompatible"; + } + if (!channelsMatch) { + extraReason = "fml.menu.multiplayer.networkincompatible"; + } + + if (fmlver < FMLNetworkConstants.FMLNETVERSION) { + extraReason = "fml.menu.multiplayer.serveroutdated"; + } + if (fmlver > FMLNetworkConstants.FMLNETVERSION) { + extraReason = "fml.menu.multiplayer.clientoutdated"; + } + target.forgeData = new ExtendedServerListData("FML", extraServerMods.isEmpty() && fmlNetMatches && channelsMatch && modsMatch, mods.size(), extraReason); + } else { + target.forgeData = new ExtendedServerListData("VANILLA", NetworkRegistry.canConnectToVanillaServer(),0, null); } } - public static void drawForgePingInfo(GuiMultiplayer gui, ServerData target, int x, int y, int width, int relativeMouseX, int relativeMouseY){ + public static void drawForgePingInfo(GuiMultiplayer gui, ServerData target, int x, int y, int width, int relativeMouseX, int relativeMouseY) { int idx; String tooltip; - if(target.forgeData == null) + if (target.forgeData == null) return; - switch (target.forgeData.type){ + switch (target.forgeData.type) { case "FML": if (target.forgeData.isCompatible) { idx = 0; @@ -115,7 +148,7 @@ public class ClientHooks } break; case "VANILLA": - if(target.forgeData.isCompatible) { + if (target.forgeData.isCompatible) { idx = 48; tooltip = ForgeI18n.parseMessage("fml.menu.multiplayer.vanilla"); } else { diff --git a/src/main/java/net/minecraftforge/fml/client/ExtendedServerListData.java b/src/main/java/net/minecraftforge/fml/client/ExtendedServerListData.java index 51c4ce988..746dfc8da 100644 --- a/src/main/java/net/minecraftforge/fml/client/ExtendedServerListData.java +++ b/src/main/java/net/minecraftforge/fml/client/ExtendedServerListData.java @@ -19,23 +19,16 @@ 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> channelData; public int numberOfMods; public String extraReason; - public ExtendedServerListData(String type, boolean isCompatible, Map> channelData, int num, String extraReason) + public ExtendedServerListData(String type, boolean isCompatible, int num, String extraReason) { this.type = type; this.isCompatible = isCompatible; - this.channelData = channelData; this.numberOfMods = num; this.extraReason = extraReason; } diff --git a/src/main/java/net/minecraftforge/fml/network/FMLNetworkConstants.java b/src/main/java/net/minecraftforge/fml/network/FMLNetworkConstants.java index 0c2dbe298..9cc72eb1b 100644 --- a/src/main/java/net/minecraftforge/fml/network/FMLNetworkConstants.java +++ b/src/main/java/net/minecraftforge/fml/network/FMLNetworkConstants.java @@ -42,4 +42,8 @@ public class FMLNetworkConstants static final ResourceLocation FML_PLAY_RESOURCE = new ResourceLocation("fml:play"); static final SimpleChannel handshakeChannel = NetworkInitialization.getHandshakeChannel(); static final SimpleChannel playChannel = NetworkInitialization.getPlayChannel(); + /** + * Return this value in your {@link net.minecraftforge.fml.ExtensionPoint#DISPLAYTEST} function to be ignored. + */ + public static final String IGNORESERVERONLY = "OHNOES\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31\uD83D\uDE31"; } diff --git a/src/main/java/net/minecraftforge/fml/network/FMLStatusPing.java b/src/main/java/net/minecraftforge/fml/network/FMLStatusPing.java index bd50d47a6..074c53055 100644 --- a/src/main/java/net/minecraftforge/fml/network/FMLStatusPing.java +++ b/src/main/java/net/minecraftforge/fml/network/FMLStatusPing.java @@ -20,80 +20,124 @@ package net.minecraftforge.fml.network; import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Maps; -import com.google.gson.*; +import com.google.gson.JsonArray; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSyntaxException; import net.minecraft.util.JsonUtils; import net.minecraft.util.ResourceLocation; +import net.minecraftforge.fml.ExtensionPoint; import net.minecraftforge.fml.ModList; -import net.minecraftforge.registries.RegistryManager; import org.apache.commons.lang3.tuple.Pair; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import java.util.HashMap; import java.util.Map; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; +import static net.minecraftforge.fml.network.FMLNetworkConstants.NETWORK; + +/** + * { + * "fmlNetworkVersion" : FMLNETVERSION, + * "channels": [ + * { + * "res": "fml:handshake", + * "version": "1.2.3.4", + * "required": true + * } + * ], + * "mods": [ + * { + * "modid": "modid", + * "modmarker": "" + * } + * ] + * } + * + */ public class FMLStatusPing { + private static final Logger LOGGER = LogManager.getLogger(); - private Map> channelVersions; - private int numberOfMods; - private int fmlNetworkVer; - - public FMLStatusPing(){ - this.channelVersions = NetworkRegistry.buildChannelVersionsForListPing(); - this.numberOfMods = ModList.get().size(); + private transient Map> channels; + private transient Map mods; + private transient int fmlNetworkVer; + public FMLStatusPing() { + this.channels = NetworkRegistry.buildChannelVersionsForListPing(); + this.mods = new HashMap<>(); + ModList.get().forEachModContainer((modid, mc) -> + mods.put(modid, mc.getCustomExtension(ExtensionPoint.DISPLAYTEST). + map(Pair::getLeft).map(Supplier::get).orElse(FMLNetworkConstants.IGNORESERVERONLY))); this.fmlNetworkVer = FMLNetworkConstants.FMLNETVERSION; } - private FMLStatusPing(Map> deserialized, int nom, int fmlNetVer){ - this.channelVersions = ImmutableMap.copyOf(deserialized); - this.numberOfMods = nom; + private FMLStatusPing(Map> deserialized, Map modMarkers, int fmlNetVer) { + this.channels = ImmutableMap.copyOf(deserialized); + this.mods = modMarkers; 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)); - } - return new FMLStatusPing(versions, JsonUtils.getInt(forgeData, "numberOfMods"), JsonUtils.getInt(forgeData, "fmlNetworkVersion")); - }catch (Exception c){ + final Map> channels = StreamSupport.stream(JsonUtils.getJsonArray(forgeData, "channels").spliterator(), false). + map(JsonElement::getAsJsonObject). + collect(Collectors.toMap(jo -> new ResourceLocation(JsonUtils.getString(jo, "res")), + jo -> Pair.of(JsonUtils.getString(jo, "version"), JsonUtils.getBoolean(jo, "required"))) + ); + + final Map mods = StreamSupport.stream(JsonUtils.getJsonArray(forgeData, "mods").spliterator(), false). + map(JsonElement::getAsJsonObject). + collect(Collectors.toMap(jo -> JsonUtils.getString(jo, "modId"), jo->JsonUtils.getString(jo, "modmarker"))); + + final int remoteFMLVersion = JsonUtils.getInt(forgeData, "fmlNetworkVersion"); + return new FMLStatusPing(channels, mods, remoteFMLVersion); + } catch (JsonSyntaxException e) { + LOGGER.debug(NETWORK, "Encountered an error parsing status ping data", e); return null; } } - public static JsonObject serialize(FMLStatusPing forgeData, JsonSerializationContext ctx){ + public static JsonObject serialize(FMLStatusPing forgeData, JsonSerializationContext ctx) { JsonObject obj = new JsonObject(); - JsonArray mods = new JsonArray(); - forgeData.channelVersions.entrySet().stream().map(p -> { + JsonArray channels = new JsonArray(); + forgeData.channels.forEach((namespace, version) -> { 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); - obj.addProperty("numberOfMods", forgeData.numberOfMods); + mi.addProperty("res", namespace.toString()); + mi.addProperty("version", version.getLeft()); + mi.addProperty("required", version.getRight()); + channels.add(mi); + }); + + obj.add("channels", channels); + + JsonArray modTestValues = new JsonArray(); + forgeData.mods.forEach((modId, value) -> { + JsonObject mi = new JsonObject(); + mi.addProperty("modId", modId); + mi.addProperty("modmarker", value); + modTestValues.add(mi); + }); + obj.add("mods", modTestValues); obj.addProperty("fmlNetworkVersion", forgeData.fmlNetworkVer); return obj; } } - public Map> getPresentMods(){ - return this.channelVersions; + public Map> getRemoteChannels() { + return this.channels; } - public int getNumberOfMods(){ - return numberOfMods; + public Map getRemoteModData() { + return mods; } - public int getFMLNetworkVersion(){ + public int getFMLNetworkVersion() { return fmlNetworkVer; } diff --git a/src/main/resources/assets/forge/lang/en_us.json b/src/main/resources/assets/forge/lang/en_us.json index 80387ad8c..1851d5d1f 100644 --- a/src/main/resources/assets/forge/lang/en_us.json +++ b/src/main/resources/assets/forge/lang/en_us.json @@ -15,14 +15,17 @@ "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.compatible":"Compatible FML modded server\n{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.incompatible.extra":"Incompatible FML modded server\n{0}", "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.multiplayer.serveroutdated":"The Forge server network version is outdated", + "fml.menu.multiplayer.clientoutdated":"The Forge client network version is outdated", + "fml.menu.multiplayer.extraservermods":"The Server has additional mods that may be needed on the client", + "fml.menu.multiplayer.modsincompatible":"The Server's mods are not compatible", + "fml.menu.multiplayer.networkincompatible":"The Server's network messages are not compatible", "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",