/* * Minecraft Forge * Copyright (c) 2016-2020. * * 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.client; import java.io.File; import java.util.*; import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; import javax.annotation.Nullable; 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.mojang.blaze3d.matrix.MatrixStack; import net.minecraft.client.entity.player.ClientPlayerEntity; import net.minecraft.client.gui.AbstractGui; import net.minecraft.client.gui.screen.MultiplayerScreen; import net.minecraft.client.multiplayer.PlayerController; import net.minecraft.util.text.StringTextComponent; import net.minecraftforge.client.event.ClientPlayerNetworkEvent; import net.minecraftforge.common.MinecraftForge; 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.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; import org.apache.logging.log4j.MarkerManager; import com.google.common.base.MoreObjects; import com.google.common.base.Strings; import net.minecraft.client.Minecraft; import net.minecraft.client.multiplayer.ServerData; import net.minecraft.client.world.ClientWorld; import net.minecraft.network.NetworkManager; import net.minecraft.network.ServerStatusResponse; import net.minecraft.resources.ResourcePack; import net.minecraft.resources.FallbackResourceManager; import net.minecraft.resources.IResourcePack; import net.minecraft.resources.SimpleReloadableResourceManager; import net.minecraft.util.ResourceLocation; import net.minecraftforge.fml.common.ObfuscationReflectionHelper; import net.minecraftforge.forgespi.language.IModInfo; import net.minecraftforge.fml.packs.ModFileResourcePack; import net.minecraftforge.registries.GameData; public class ClientHooks { private static final Logger LOGGER = LogManager.getLogger(); private static final Marker CLIENTHOOKS = MarkerManager.getMarker("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) { final Map mods = packet.getForgeData().getRemoteModData(); final Map> remoteChannels = packet.getForgeData().getRemoteChannels(); final int fmlver = packet.getForgeData().getFMLNetworkVersion(); boolean fmlNetMatches = fmlver == FMLNetworkConstants.FMLNETVERSION; boolean channelsMatch = NetworkRegistry.checkListPingCompatibilityForClient(remoteChannels); AtomicBoolean result = new AtomicBoolean(true); final List extraClientMods = new ArrayList<>(); ModList.get().forEachModContainer((modid, mc) -> mc.getCustomExtension(ExtensionPoint.DISPLAYTEST).ifPresent(ext-> { boolean foundModOnServer = ext.getRight().test(mods.get(modid), true); result.compareAndSet(true, foundModOnServer); if (!foundModOnServer) { extraClientMods.add(modid); } }) ); boolean modsMatch = result.get(); 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 (!extraServerMods.isEmpty()) { extraReason = "fml.menu.multiplayer.extraservermods"; LOGGER.info(CLIENTHOOKS, ForgeI18n.parseMessage(extraReason) + ": {}", extraServerMods.entrySet().stream() .map(e -> e.getKey() + "@" + e.getValue()) .collect(Collectors.joining(", "))); } if (!modsMatch) { extraReason = "fml.menu.multiplayer.modsincompatible"; LOGGER.info(CLIENTHOOKS, "Client has mods that are missing on server: {}", extraClientMods); } 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"; } if (!packet.getForgeData().isPatchAdvertised()) { extraReason = "fml.menu.multiplayer.serverunpatched"; } 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(MultiplayerScreen gui, ServerData target, MatrixStack mStack, int x, int y, int width, int relativeMouseX, int relativeMouseY) { int idx; String tooltip; if (target.forgeData == null) return; switch (target.forgeData.type) { case "FML": if (target.forgeData.isCompatible) { // HACK: Allow connections to unpatched servers, but show a warning if (target.forgeData.extraReason != null && target.forgeData.extraReason.equals("fml.menu.multiplayer.serverunpatched")) { idx = 96; tooltip = ForgeI18n.parseMessage("fml.menu.multiplayer.incompatible.extra", ForgeI18n.parseMessage(target.forgeData.extraReason)); } else { idx = 0; tooltip = ForgeI18n.parseMessage("fml.menu.multiplayer.compatible", target.forgeData.numberOfMods); } } else { idx = 16; if(target.forgeData.extraReason != null) { if (target.forgeData.extraReason.equals("fml.menu.multiplayer.serverunpatched")) idx = 96; 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 = ForgeI18n.parseMessage("fml.menu.multiplayer.unknown", target.forgeData.type); } Minecraft.getInstance().getTextureManager().bindTexture(iconSheet); AbstractGui.blit(mStack, x + width - 18, y + 10, 16, 16, 0, idx, 16, 16, 256, 256); if(relativeMouseX > width - 15 && relativeMouseX < width && relativeMouseY > 10 && relativeMouseY < 26) //TODO using StringTextComponent here is a hack, we should be using TranslationTextComponents. gui.func_238854_b_(Collections.singletonList(new StringTextComponent(tooltip))); } public static String fixDescription(String description) { return description.endsWith(":NOFML§r") ? description.substring(0, description.length() - 8)+"§r" : description; } @SuppressWarnings("resource") static File getSavesDir() { return new File(Minecraft.getInstance().gameDir, "saves"); } private static NetworkManager getClientToServerNetworkManager() { return Minecraft.getInstance().getConnection()!=null ? Minecraft.getInstance().getConnection().getNetworkManager() : null; } public static void handleClientWorldClosing(ClientWorld world) { NetworkManager client = getClientToServerNetworkManager(); // ONLY revert a non-local connection if (client != null && !client.isLocalChannel()) { GameData.revertToFrozen(); } } private static SetMultimap missingTextures = HashMultimap.create(); private static Set badTextureDomains = Sets.newHashSet(); private static Table> brokenTextures = HashBasedTable.create(); public static void trackMissingTexture(ResourceLocation resourceLocation) { badTextureDomains.add(resourceLocation.getNamespace()); missingTextures.put(resourceLocation.getNamespace(),resourceLocation); } public static void trackBrokenTexture(ResourceLocation resourceLocation, String error) { badTextureDomains.add(resourceLocation.getNamespace()); Set badType = brokenTextures.get(resourceLocation.getNamespace(), error); if (badType == null) { badType = Sets.newHashSet(); brokenTextures.put(resourceLocation.getNamespace(), MoreObjects.firstNonNull(error, "Unknown error"), badType); } badType.add(resourceLocation); } public static void logMissingTextureErrors() { if (missingTextures.isEmpty() && brokenTextures.isEmpty()) { return; } Logger logger = LogManager.getLogger("FML.TEXTURE_ERRORS"); logger.error(Strings.repeat("+=", 25)); logger.error("The following texture errors were found."); Map resManagers = ObfuscationReflectionHelper.getPrivateValue(SimpleReloadableResourceManager.class, (SimpleReloadableResourceManager)Minecraft.getInstance().getResourceManager(), "field_199014"+"_c"); for (String resourceDomain : badTextureDomains) { Set missing = missingTextures.get(resourceDomain); logger.error(Strings.repeat("=", 50)); logger.error(" DOMAIN {}", resourceDomain); logger.error(Strings.repeat("-", 50)); logger.error(" domain {} is missing {} texture{}",resourceDomain, missing.size(),missing.size()!=1 ? "s" : ""); FallbackResourceManager fallbackResourceManager = resManagers.get(resourceDomain); if (fallbackResourceManager == null) { logger.error(" domain {} is missing a resource manager - it is probably a side-effect of automatic texture processing", resourceDomain); } else { List resPacks = fallbackResourceManager.resourcePacks; logger.error(" domain {} has {} location{}:",resourceDomain, resPacks.size(), resPacks.size() != 1 ? "s" :""); for (IResourcePack resPack : resPacks) { if (resPack instanceof ModFileResourcePack) { ModFileResourcePack modRP = (ModFileResourcePack) resPack; List mods = modRP.getModFile().getModInfos(); logger.error(" mod(s) {} resources at {}", mods.stream().map(IModInfo::getDisplayName).collect(Collectors.toList()), modRP.getModFile().getFilePath()); } else if (resPack instanceof ResourcePack) { logger.error(" resource pack at path {}", ((ResourcePack)resPack).file.getPath()); } else { logger.error(" unknown resourcepack type {} : {}", resPack.getClass().getName(), resPack.getName()); } } } logger.error(Strings.repeat("-", 25)); if (missingTextures.containsKey(resourceDomain)) { logger.error(" The missing resources for domain {} are:", resourceDomain); for (ResourceLocation rl : missing) { logger.error(" {}", rl.getPath()); } logger.error(Strings.repeat("-", 25)); } if (!brokenTextures.containsRow(resourceDomain)) { logger.error(" No other errors exist for domain {}", resourceDomain); } else { logger.error(" The following other errors were reported for domain {}:",resourceDomain); Map> resourceErrs = brokenTextures.row(resourceDomain); for (String error: resourceErrs.keySet()) { logger.error(Strings.repeat("-", 25)); logger.error(" Problem: {}", error); for (ResourceLocation rl : resourceErrs.get(error)) { logger.error(" {}",rl.getPath()); } } } logger.error(Strings.repeat("=", 50)); } logger.error(Strings.repeat("+=", 25)); } public static void firePlayerLogin(PlayerController pc, ClientPlayerEntity player, NetworkManager networkManager) { MinecraftForge.EVENT_BUS.post(new ClientPlayerNetworkEvent.LoggedInEvent(pc, player, networkManager)); } public static void firePlayerLogout(PlayerController pc, ClientPlayerEntity player) { MinecraftForge.EVENT_BUS.post(new ClientPlayerNetworkEvent.LoggedOutEvent(pc, player, player != null ? player.connection != null ? player.connection.getNetworkManager() : null : null)); } public static void firePlayerRespawn(PlayerController pc, ClientPlayerEntity oldPlayer, ClientPlayerEntity newPlayer, NetworkManager networkManager) { MinecraftForge.EVENT_BUS.post(new ClientPlayerNetworkEvent.RespawnEvent(pc, oldPlayer, newPlayer, networkManager)); } }