diff --git a/ReplayStudio b/ReplayStudio index eaeb1966..040e61d7 160000 --- a/ReplayStudio +++ b/ReplayStudio @@ -1 +1 @@ -Subproject commit eaeb19666e61b5efd2ab92f5c852d85209506857 +Subproject commit 040e61d71d5d8afc2b2f1d9e4d6f3bc02a36e353 diff --git a/src/main/java/com/replaymod/core/utils/ModCompat.java b/src/main/java/com/replaymod/core/utils/ModCompat.java new file mode 100644 index 00000000..876c05fe --- /dev/null +++ b/src/main/java/com/replaymod/core/utils/ModCompat.java @@ -0,0 +1,61 @@ +package com.replaymod.core.utils; + +import com.replaymod.replaystudio.data.ModInfo; +import net.minecraft.util.ResourceLocation; +import net.minecraftforge.fml.common.Loader; +import net.minecraftforge.fml.common.ModContainer; +import net.minecraftforge.fml.common.registry.GameData; + +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class ModCompat { + @SuppressWarnings("unchecked") + public static Collection getInstalledNetworkMods() { + Map ignoreCaseMap = Loader.instance().getModList().stream() + .collect(Collectors.toMap(m -> m.getModId().toLowerCase(), Function.identity())); + return Stream.concat( + ((Set) GameData.getBlockRegistry().getKeys()).stream(), + ((Set) GameData.getItemRegistry().getKeys()).stream() + ).map(ResourceLocation::getResourceDomain).filter(s -> !s.equals("minecraft")).distinct() + .map(String::toLowerCase).map(ignoreCaseMap::get).filter(mod -> mod != null) + .map(mod -> new ModInfo(mod.getModId(), mod.getName(), mod.getVersion())) + .collect(Collectors.toList()); + } + + public static final class ModInfoDifference { + private final Set missing = new HashSet<>(); + private final Map differing = new HashMap<>(); + public ModInfoDifference(Collection requiredList) { + Collection installedList = getInstalledNetworkMods(); + REQUIRED: + for (ModInfo required : requiredList) { + for (ModInfo installed : installedList) { + if (required.getId().equals(installed.getId())) { + // Mod is installed, check if versions match + if (Objects.equals(required.getVersion(), installed.getVersion())) { + // Mod found and version match + continue REQUIRED; + } else { + // Mod found but versions don't match + differing.put(required, installed.getVersion()); + continue REQUIRED; + } + } + } + // Mod no longer installed + missing.add(required); + } + } + + public Set getMissing() { + return Collections.unmodifiableSet(missing); + } + + public Map getDiffering() { + return Collections.unmodifiableMap(differing); + } + } +} diff --git a/src/main/java/com/replaymod/recording/handler/ConnectionEventHandler.java b/src/main/java/com/replaymod/recording/handler/ConnectionEventHandler.java index b25f480c..ad9d7b46 100755 --- a/src/main/java/com/replaymod/recording/handler/ConnectionEventHandler.java +++ b/src/main/java/com/replaymod/recording/handler/ConnectionEventHandler.java @@ -1,6 +1,7 @@ package com.replaymod.recording.handler; import com.replaymod.core.ReplayMod; +import com.replaymod.core.utils.ModCompat; import com.replaymod.recording.Setting; import com.replaymod.recording.gui.GuiRecordingOverlay; import com.replaymod.recording.packet.PacketListener; @@ -83,6 +84,8 @@ public class ConnectionEventHandler { File currentFile = new File(folder, name + ".mcpr"); ReplayFile replayFile = new ZipReplayFile(new ReplayStudio(), currentFile); + replayFile.writeModInfo(ModCompat.getInstalledNetworkMods()); + ReplayMetaData metaData = new ReplayMetaData(); metaData.setSingleplayer(event.isLocal); metaData.setServerName(worldName); diff --git a/src/main/java/com/replaymod/replay/ReplayModReplay.java b/src/main/java/com/replaymod/replay/ReplayModReplay.java index a7041ab6..44718b54 100644 --- a/src/main/java/com/replaymod/replay/ReplayModReplay.java +++ b/src/main/java/com/replaymod/replay/ReplayModReplay.java @@ -5,8 +5,10 @@ import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.replaymod.core.ReplayMod; +import com.replaymod.core.utils.ModCompat; import com.replaymod.replay.camera.*; import com.replaymod.replay.gui.overlay.GuiMarkerTimeline; +import com.replaymod.replay.gui.screen.GuiModCompatWarning; import com.replaymod.replay.handler.GuiHandler; import com.replaymod.replaystudio.data.Marker; import com.replaymod.replaystudio.replay.ReplayFile; @@ -165,6 +167,17 @@ public class ReplayModReplay { } public void startReplay(ReplayFile replayFile) throws IOException { + startReplay(replayFile, true); + } + + public void startReplay(ReplayFile replayFile, boolean checkModCompat) throws IOException { + if (checkModCompat) { + ModCompat.ModInfoDifference modDifference = new ModCompat.ModInfoDifference(replayFile.getModInfo()); + if (!modDifference.getMissing().isEmpty() || !modDifference.getDiffering().isEmpty()) { + new GuiModCompatWarning(this, replayFile, modDifference).display(); + return; + } + } replayHandler = new ReplayHandler(replayFile, true); } diff --git a/src/main/java/com/replaymod/replay/gui/screen/GuiModCompatWarning.java b/src/main/java/com/replaymod/replay/gui/screen/GuiModCompatWarning.java new file mode 100644 index 00000000..dbd40b52 --- /dev/null +++ b/src/main/java/com/replaymod/replay/gui/screen/GuiModCompatWarning.java @@ -0,0 +1,98 @@ +package com.replaymod.replay.gui.screen; + +import com.replaymod.core.utils.ModCompat; +import com.replaymod.replay.ReplayModReplay; +import com.replaymod.replaystudio.data.ModInfo; +import com.replaymod.replaystudio.replay.ReplayFile; +import com.replaymod.replaystudio.util.I18n; +import de.johni0702.minecraft.gui.container.AbstractGuiScreen; +import de.johni0702.minecraft.gui.container.GuiPanel; +import de.johni0702.minecraft.gui.container.GuiVerticalList; +import de.johni0702.minecraft.gui.element.GuiButton; +import de.johni0702.minecraft.gui.element.GuiLabel; +import de.johni0702.minecraft.gui.layout.CustomLayout; +import de.johni0702.minecraft.gui.layout.HorizontalLayout; +import de.johni0702.minecraft.gui.layout.VerticalLayout; + +import java.io.IOException; +import java.util.Map; + +public class GuiModCompatWarning extends AbstractGuiScreen { + public final GuiVerticalList content = new GuiVerticalList(this).setDrawShadow(true).setDrawSlider(true); + public final GuiButton loadButton = new GuiButton().setI18nLabel("replaymod.gui.load").setSize(200, 20); + public final GuiButton cancelButton = new GuiButton().setI18nLabel("gui.cancel").setSize(200, 20); + public final GuiPanel closeButtons = new GuiPanel(this).setLayout(new HorizontalLayout().setSpacing(5)) + .addElements(null, loadButton, cancelButton); + + { + setTitle(new GuiLabel().setI18nText("replaymod.gui.modwarning.title")); + setLayout(new CustomLayout() { + @Override + protected void layout(GuiModCompatWarning container, int width, int height) { + pos(content, 10, 35); + pos(closeButtons, width / 2 - width(closeButtons) / 2, height - 10 - height(closeButtons)); + size(content, width - 20, y(closeButtons) - 10 - y(content)); + } + }); + + content.getListLayout().setSpacing(8); + } + + public GuiModCompatWarning(ReplayModReplay mod, ReplayFile replayFile, ModCompat.ModInfoDifference difference) { + VerticalLayout.Data data = new VerticalLayout.Data(0.5); + GuiPanel content = this.content.getListPanel(); + content.addElements(data, new GuiLabel().setI18nText("replaymod.gui.modwarning.message1")); + content.addElements(data, new GuiLabel().setI18nText("replaymod.gui.modwarning.message2")); + content.addElements(data, new GuiLabel().setI18nText("replaymod.gui.modwarning.message3")); + + if (!difference.getMissing().isEmpty()) { + content.addElements(data, new GuiLabel()); + content.addElements(data, new GuiLabel().setI18nText("replaymod.gui.modwarning.missing")); + for (ModInfo modInfo : difference.getMissing()) { + content.addElements(data, new GuiPanel().setLayout(new VerticalLayout().setSpacing(3)).addElements(null, + GuiPanel.builder().layout(new HorizontalLayout().setSpacing(3)) + .with(new GuiLabel().setI18nText("replaymod.gui.modwarning.name"), null) + .with(new GuiLabel().setText(modInfo.getName()), null).build(), + GuiPanel.builder().layout(new HorizontalLayout().setSpacing(3)) + .with(new GuiLabel().setI18nText("replaymod.gui.modwarning.id"), null) + .with(new GuiLabel().setText(modInfo.getId()), null).build(), + new GuiLabel().setText(I18n.format("replaymod.gui.modwarning.version.expected") + + ": " + modInfo.getVersion()) + )); + } + } + + if (!difference.getDiffering().isEmpty()) { + content.addElements(data, new GuiLabel()); + content.addElements(data, new GuiLabel().setI18nText("replaymod.gui.modwarning.version")); + for (Map.Entry entry : difference.getDiffering().entrySet()) { + content.addElements(data, new GuiPanel().setLayout(new VerticalLayout().setSpacing(3)).addElements(null, + GuiPanel.builder().layout(new HorizontalLayout().setSpacing(3)) + .with(new GuiLabel().setI18nText("replaymod.gui.modwarning.name"), null) + .with(new GuiLabel().setText(entry.getKey().getName()), null).build(), + GuiPanel.builder().layout(new HorizontalLayout().setSpacing(3)) + .with(new GuiLabel().setI18nText("replaymod.gui.modwarning.id"), null) + .with(new GuiLabel().setText(entry.getKey().getId()), null).build(), + new GuiLabel().setText(I18n.format("replaymod.gui.modwarning.version.expected") + + ": " + entry.getKey().getVersion() + + ", " + I18n.format("replaymod.gui.modwarning.version.found") + + ": " + entry.getValue()) + )); + } + } + + cancelButton.onClick(() -> getMinecraft().displayGuiScreen(null)); + loadButton.onClick(() -> { + try { + mod.startReplay(replayFile, false); + } catch (IOException e) { + e.printStackTrace(); + } + }); + } + + @Override + protected GuiModCompatWarning getThis() { + return this; + } +} diff --git a/src/main/resources/assets/replaymod/lang/en_US.lang b/src/main/resources/assets/replaymod/lang/en_US.lang index 27c5a99a..bee76354 100644 --- a/src/main/resources/assets/replaymod/lang/en_US.lang +++ b/src/main/resources/assets/replaymod/lang/en_US.lang @@ -484,4 +484,16 @@ replaymod.gui.objects=Custom Objects #Errors replaymod.error.unknownrestriction1=This replay cannot be played with your current version. -replaymod.error.unknownrestriction2=It tried to enforce %s which is unknown. \ No newline at end of file +replaymod.error.unknownrestriction2=It tried to enforce %s which is unknown. + +#Replay Mod Incompatibility Warning +replaymod.gui.modwarning.title=Incompatibility detected +replaymod.gui.modwarning.message1=A possible incompatibility between your current +replaymod.gui.modwarning.message2=Minecraft version and this Replay has been detected. +replaymod.gui.modwarning.message3=Loading this Replay may result in errors or a crash. +replaymod.gui.modwarning.name=Mod Name: +replaymod.gui.modwarning.id=Mod Id: +replaymod.gui.modwarning.missing=Missing Mods +replaymod.gui.modwarning.version=Different Mod Versions +replaymod.gui.modwarning.version.expected=Expected +replaymod.gui.modwarning.version.found=Found \ No newline at end of file