Implement API for mods to control their server status response..

Signed-off-by: cpw <cpw+github@weeksfamily.ca>
This commit is contained in:
cpw 2019-03-31 16:36:49 -04:00
parent 4ecb04ed9f
commit 3f735b715b
No known key found for this signature in database
GPG Key ID: 8EB3DF749553B1B7
10 changed files with 183 additions and 76 deletions

View File

@ -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;

View File

@ -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));
}
/*

View File

@ -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<T>
{
public static final ExtensionPoint<BiFunction<Minecraft, GuiScreen, GuiScreen>> CONFIGGUIFACTORY = new ExtensionPoint<>();
public static final ExtensionPoint<BiFunction<Minecraft, ModFileResourcePack, IResourcePack>> 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<Pair<Supplier<String>, BiPredicate<String, Boolean>>> DISPLAYTEST = new ExtensionPoint<>();
/**
* Register with {@link ModLoadingContext#}
* Register with {@link ModLoadingContext#registerExtensionPoint(ExtensionPoint, Supplier)}
*/
public static final ExtensionPoint<Function<FMLPlayMessages.OpenContainer, GuiScreen>> GUIFACTORY = new ExtensionPoint<>();

View File

@ -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())));
}
/**

View File

@ -183,4 +183,8 @@ public class ModList
{
modFiles.stream().map(ModFileInfo::getFile).forEach(fileConsumer);
}
public void forEachModContainer(BiConsumer<String, ModContainer> modContainerConsumer) {
indexedMods.forEach(modContainerConsumer);
}
}

View File

@ -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<String, String> mods = packet.getForgeData().getRemoteModData();
final Map<ResourceLocation, Pair<String, Boolean>> 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<String, String> 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.serveroutdated";
else 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 {

View File

@ -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<ResourceLocation, Pair<String, Boolean>> channelData;
public int numberOfMods;
public String extraReason;
public ExtendedServerListData(String type, boolean isCompatible, Map<ResourceLocation, Pair<String, Boolean>> 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;
}

View File

@ -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";
}

View File

@ -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": "<somestring>"
* }
* ]
* }
*
*/
public class FMLStatusPing {
private static final Logger LOGGER = LogManager.getLogger();
private Map<ResourceLocation, Pair<String, Boolean>> channelVersions;
private int numberOfMods;
private int fmlNetworkVer;
public FMLStatusPing(){
this.channelVersions = NetworkRegistry.buildChannelVersionsForListPing();
this.numberOfMods = ModList.get().size();
private transient Map<ResourceLocation, Pair<String, Boolean>> channels;
private transient Map<String, String> 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<ResourceLocation, Pair<String, Boolean>> deserialized, int nom, int fmlNetVer){
this.channelVersions = ImmutableMap.copyOf(deserialized);
this.numberOfMods = nom;
private FMLStatusPing(Map<ResourceLocation, Pair<String, Boolean>> deserialized, Map<String,String> 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<ResourceLocation, Pair<String, Boolean>> 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<ResourceLocation, Pair<String, Boolean>> 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<String, String> 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<ResourceLocation, Pair<String, Boolean>> getPresentMods(){
return this.channelVersions;
public Map<ResourceLocation, Pair<String, Boolean>> getRemoteChannels() {
return this.channels;
}
public int getNumberOfMods(){
return numberOfMods;
public Map<String,String> getRemoteModData() {
return mods;
}
public int getFMLNetworkVersion(){
public int getFMLNetworkVersion() {
return fmlNetworkVer;
}

View File

@ -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",