Introduce minimal mode (allows RM to run on unsupported MC versions)

by disabling various version-specific features (i.e. everything which
requires the ReplayStudio).
This commit is contained in:
Jonas Herzig
2019-06-20 16:50:33 +02:00
parent 357501e5db
commit af55951f42
13 changed files with 195 additions and 36 deletions

View File

@@ -14,6 +14,7 @@ import com.replaymod.online.ReplayModOnline;
import com.replaymod.recording.ReplayModRecording; import com.replaymod.recording.ReplayModRecording;
import com.replaymod.render.ReplayModRender; import com.replaymod.render.ReplayModRender;
import com.replaymod.replay.ReplayModReplay; import com.replaymod.replay.ReplayModReplay;
import com.replaymod.replaystudio.studio.ReplayStudio;
import com.replaymod.replaystudio.util.I18n; import com.replaymod.replaystudio.util.I18n;
import com.replaymod.simplepathing.ReplayModSimplePathing; import com.replaymod.simplepathing.ReplayModSimplePathing;
import de.johni0702.minecraft.gui.container.GuiScreen; import de.johni0702.minecraft.gui.container.GuiScreen;
@@ -157,6 +158,16 @@ public class ReplayMod implements
private final GuiBackgroundProcesses backgroundProcesses = new GuiBackgroundProcesses(); private final GuiBackgroundProcesses backgroundProcesses = new GuiBackgroundProcesses();
/**
* Whether the current MC version is supported by the embedded ReplayStudio version.
* If this is not the case (i.e. if this is variable true), any feature of the RM which depends on the ReplayStudio
* lib will be disabled.
*
* Only supported on Fabric builds, i.e. will always be false / crash the game with Forge/pre-1.14 builds.
* (specifically the code below and MCVer#getProtocolVersion make this assumption)
*/
private boolean minimalMode;
public ReplayMod() { public ReplayMod() {
I18n.setI18n(net.minecraft.client.resource.language.I18n::translate); I18n.setI18n(net.minecraft.client.resource.language.I18n::translate);
@@ -164,12 +175,17 @@ public class ReplayMod implements
// Check Minecraft protocol version for compatibility // Check Minecraft protocol version for compatibility
int supportedProtocol = MinecraftConstants.PROTOCOL_VERSION; int supportedProtocol = MinecraftConstants.PROTOCOL_VERSION;
int actualProtocol = SharedConstants.getGameVersion().getProtocolVersion(); int actualProtocol = SharedConstants.getGameVersion().getProtocolVersion();
if (supportedProtocol != actualProtocol) { if (supportedProtocol != actualProtocol && !Boolean.parseBoolean(System.getProperty("replaymod.skipversioncheck", "false"))) {
throw new UnsupportedOperationException(String.format( minimalMode = true;
"Unsupported Minecraft version, supporting protocol version %s (%s) but actual version is %s (%s).", // Only allow use of older RM on newer (e.g. snapshot, pre-release) versions.
supportedProtocol, MinecraftConstants.GAME_VERSION, // The other way around is almost certainly user error.
actualProtocol, SharedConstants.getGameVersion().getName() if (actualProtocol < supportedProtocol && !Boolean.parseBoolean(System.getProperty("replaymod.allowoldsnapshot", "false"))) {
)); throw new UnsupportedOperationException(String.format(
"Unsupported Minecraft version, supporting protocol version %s (%s) but actual version is %s (%s).",
supportedProtocol, MinecraftConstants.GAME_VERSION,
actualProtocol, SharedConstants.getGameVersion().getName()
));
}
} }
//#endif //#endif
@@ -602,4 +618,17 @@ public class ReplayMod implements
public GuiBackgroundProcesses getBackgroundProcesses() { public GuiBackgroundProcesses getBackgroundProcesses() {
return backgroundProcesses; return backgroundProcesses;
} }
// This method is static because it depends solely on the environment, not on the actual RM instance.
public static boolean isMinimalMode() {
return ReplayMod.instance.minimalMode;
}
public static boolean isCompatible(int fileFormatVersion, int protocolVersion) {
if (isMinimalMode()) {
return protocolVersion == MCVer.getProtocolVersion();
} else {
return new ReplayStudio().isCompatible(fileFormatVersion, protocolVersion);
}
}
} }

View File

@@ -1,7 +1,7 @@
package com.replaymod.core.gui; package com.replaymod.core.gui;
import com.google.common.io.Files; import com.google.common.io.Files;
import com.replaymod.replaystudio.PacketData; import com.google.gson.Gson;
import com.replaymod.replaystudio.io.ReplayInputStream; import com.replaymod.replaystudio.io.ReplayInputStream;
import com.replaymod.replaystudio.io.ReplayOutputStream; import com.replaymod.replaystudio.io.ReplayOutputStream;
import com.replaymod.replaystudio.replay.ReplayFile; import com.replaymod.replaystudio.replay.ReplayFile;
@@ -17,9 +17,14 @@ import de.johni0702.minecraft.gui.layout.CustomLayout;
import de.johni0702.minecraft.gui.layout.HorizontalLayout; import de.johni0702.minecraft.gui.layout.HorizontalLayout;
import de.johni0702.minecraft.gui.layout.VerticalLayout; import de.johni0702.minecraft.gui.layout.VerticalLayout;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream;
import static com.replaymod.replaystudio.util.Utils.readInt;
import static com.replaymod.replaystudio.util.Utils.writeInt;
public class RestoreReplayGui extends AbstractGuiScreen<RestoreReplayGui> { public class RestoreReplayGui extends AbstractGuiScreen<RestoreReplayGui> {
@@ -53,16 +58,34 @@ public class RestoreReplayGui extends AbstractGuiScreen<RestoreReplayGui> {
// We need to re-write the packet data in case there are any incomplete packets dangling at the end // We need to re-write the packet data in case there are any incomplete packets dangling at the end
try (ReplayInputStream in = replayFile.getPacketData(studio, true); try (ReplayInputStream in = replayFile.getPacketData(studio, true);
ReplayOutputStream out = replayFile.writePacketData(true)) { ReplayOutputStream out = replayFile.writePacketData(true)) {
PacketData last = null; while (true) {
while ((last = in.readPacket()) != null) { // To prevent failing at un-parsable packets and to support recovery in minimal mode,
metaData.setDuration((int) last.getTime()); // we do not use the ReplayIn/OutputStream methods but instead parse the packets ourselves.
out.write(last); int time = readInt(in);
int length = readInt(in);
if (time == -1 || length == -1) {
break;
}
byte[] buf = new byte[length];
IOUtils.readFully(in, buf);
// Fully read, update replay duration
metaData.setDuration(time);
// Write packet back into recovered replay
writeInt(out, time);
writeInt(out, length);
out.write(buf);
} }
} catch (Throwable t) { } catch (Throwable t) {
t.printStackTrace(); t.printStackTrace();
} }
// Write back the actual duration // Write back the actual duration
replayFile.writeMetaData(metaData); try (OutputStream out = replayFile.write("metaData.json")) {
metaData.setGenerator(metaData.getGenerator() + "(+ ReplayMod Replay Recovery)");
String json = (new Gson()).toJson(metaData);
out.write(json.getBytes());
}
} }
replayFile.save(); replayFile.save();
replayFile.close(); replayFile.close();

View File

@@ -5,6 +5,7 @@ import com.google.common.net.PercentEscaper;
import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.replaymod.core.ReplayMod;
import de.johni0702.minecraft.gui.GuiRenderer; import de.johni0702.minecraft.gui.GuiRenderer;
import de.johni0702.minecraft.gui.RenderInfo; import de.johni0702.minecraft.gui.RenderInfo;
import de.johni0702.minecraft.gui.container.AbstractGuiScrollable; import de.johni0702.minecraft.gui.container.AbstractGuiScrollable;
@@ -26,6 +27,7 @@ import net.minecraft.util.crash.CrashReport;
import net.minecraft.util.Identifier; import net.minecraft.util.Identifier;
import org.apache.commons.io.Charsets; import org.apache.commons.io.Charsets;
import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.FilenameUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
//#if MC>=11300 //#if MC>=11300
@@ -34,10 +36,11 @@ import org.apache.logging.log4j.Logger;
//#endif //#endif
//#if MC>=10800 //#if MC>=10800
import com.github.steveice10.mc.protocol.MinecraftConstants;
import net.minecraft.client.network.PlayerListEntry; import net.minecraft.client.network.PlayerListEntry;
import net.minecraft.client.util.DefaultSkinHelper; import net.minecraft.client.util.DefaultSkinHelper;
//#else //#else
//$$ import com.github.steveice10.mc.protocol.ProtocolConstants;
//$$ import net.minecraft.client.Minecraft; //$$ import net.minecraft.client.Minecraft;
//$$ import net.minecraft.client.entity.AbstractClientPlayer; //$$ import net.minecraft.client.entity.AbstractClientPlayer;
//$$ import net.minecraft.entity.player.EntityPlayer; //$$ import net.minecraft.entity.player.EntityPlayer;
@@ -71,6 +74,7 @@ import java.util.function.Consumer;
import static com.replaymod.core.versions.MCVer.getMinecraft; import static com.replaymod.core.versions.MCVer.getMinecraft;
public class Utils { public class Utils {
private static Logger LOGGER = LogManager.getLogger();
private static InputStream getResourceAsStream(String path) { private static InputStream getResourceAsStream(String path) {
// FIXME this seems broken in 1.13, hence the workaround. probably want to open an issue with modlauncher (or forge?) // FIXME this seems broken in 1.13, hence the workaround. probably want to open an issue with modlauncher (or forge?)
@@ -315,4 +319,62 @@ public class Utils {
throw (Error) t; throw (Error) t;
} }
} }
public static void denyIfMinimalMode(GuiContainer container, Runnable onPopupClosed, Runnable orElseRun) {
if (isNotMinimalModeElsePopup(container, onPopupClosed)) {
orElseRun.run();
}
}
public static boolean ifMinimalModeDoPopup(GuiContainer container, Runnable onPopupClosed) {
return !isNotMinimalModeElsePopup(container, onPopupClosed);
}
public static boolean isNotMinimalModeElsePopup(GuiContainer container, Runnable onPopupClosed) {
if (!ReplayMod.isMinimalMode()) {
LOGGER.trace("Minimal mode not active, continuing");
return true;
}
LOGGER.trace("Minimal mode active, denying action, opening popup");
MinimalModeUnsupportedPopup popup = new MinimalModeUnsupportedPopup(container);
Futures.addCallback(popup.getFuture(), new FutureCallback<Void>() {
@Override
public void onSuccess(@Nullable Void result) {
LOGGER.trace("Minimal mode popup closed");
if (onPopupClosed != null) {
onPopupClosed.run();
}
}
@Override
public void onFailure(Throwable t) {
LOGGER.error("During minimal mode popup:", t);
}
});
return false;
}
private static class MinimalModeUnsupportedPopup extends GuiInfoPopup {
private MinimalModeUnsupportedPopup(GuiContainer container) {
super(container);
setBackgroundColor(Colors.DARK_TRANSPARENT);
getInfo().addElements(new VerticalLayout.Data(0.5),
new GuiLabel()
.setColor(Colors.BLACK)
.setI18nText("replaymod.gui.minimalmode.unsupported"),
new GuiLabel()
.setColor(Colors.BLACK)
.setI18nText("replaymod.gui.minimalmode.supportedversion",
//#if MC>=10800
MinecraftConstants.GAME_VERSION
//#else
//$$ ProtocolConstants.GAME_VERSION
//#endif
));
open();
}
}
} }

View File

@@ -78,6 +78,7 @@ import net.minecraft.client.render.VertexFormatElement;
//#if MC>=11400 //#if MC>=11400
import net.fabricmc.loader.api.FabricLoader; import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.SharedConstants;
//#else //#else
//#if MC>=11300 //#if MC>=11300
//$$ import net.minecraftforge.fml.ModList; //$$ import net.minecraftforge.fml.ModList;
@@ -117,6 +118,14 @@ public class MCVer {
//#endif //#endif
} }
public static int getProtocolVersion() {
//#if MC>=11400
return SharedConstants.getGameVersion().getProtocolVersion();
//#else
//$$ throw new UnsupportedOperationException("Minimal mode not supported pre-1.14");
//#endif
}
public static void addDetail(CrashReportSection category, String name, Callable<String> callable) { public static void addDetail(CrashReportSection category, String name, Callable<String> callable) {
//#if MC>=10904 //#if MC>=10904
//#if MC>=11200 //#if MC>=11200

View File

@@ -41,6 +41,7 @@ public class GuiHandler extends EventRegistrations {
// Inject Edit button // Inject Edit button
if (!replayViewer.editorButton.getChildren().isEmpty()) return; if (!replayViewer.editorButton.getChildren().isEmpty()) return;
replayViewer.replaySpecificButtons.add(new GuiButton(replayViewer.editorButton).onClick(() -> { replayViewer.replaySpecificButtons.add(new GuiButton(replayViewer.editorButton).onClick(() -> {
if (Utils.ifMinimalModeDoPopup(replayViewer, () -> {})) return;
try { try {
new GuiEditReplay(replayViewer, replayViewer.list.getSelected().file.toPath()) { new GuiEditReplay(replayViewer, replayViewer.list.getSelected().file.toPath()) {
@Override @Override

View File

@@ -5,6 +5,7 @@ import com.google.common.base.Supplier;
import com.google.common.primitives.Ints; import com.google.common.primitives.Ints;
import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.replaymod.core.ReplayMod;
import com.replaymod.core.utils.Utils; import com.replaymod.core.utils.Utils;
import com.replaymod.online.ReplayModOnline; import com.replaymod.online.ReplayModOnline;
import com.replaymod.online.api.ApiClient; import com.replaymod.online.api.ApiClient;
@@ -425,7 +426,7 @@ public class GuiReplayCenter extends GuiScreen {
} else { } else {
server.setText(metaData.getServerName()); server.setText(metaData.getServerName());
} }
incompatible = !new ReplayStudio().isCompatible(fileInfo.getMetadata().getFileFormatVersion()); incompatible = !ReplayMod.isCompatible(fileInfo.getMetadata().getFileFormatVersion(), -1); // TODO protocol version support on website
if (incompatible) { if (incompatible) {
version.setText("Minecraft " + fileInfo.getMetadata().getMcVersion()); version.setText("Minecraft " + fileInfo.getMetadata().getMcVersion());
} }

View File

@@ -1,6 +1,7 @@
package com.replaymod.recording.gui; package com.replaymod.recording.gui;
import com.replaymod.core.ReplayMod; import com.replaymod.core.ReplayMod;
import com.replaymod.core.utils.Utils;
import com.replaymod.editor.gui.MarkerProcessor; import com.replaymod.editor.gui.MarkerProcessor;
import com.replaymod.recording.Setting; import com.replaymod.recording.Setting;
import com.replaymod.recording.packet.PacketListener; import com.replaymod.recording.packet.PacketListener;
@@ -32,6 +33,7 @@ public class GuiRecordingControls extends EventRegistrations {
private GuiPanel panel = new GuiPanel().setLayout(new HorizontalLayout().setSpacing(4)); private GuiPanel panel = new GuiPanel().setLayout(new HorizontalLayout().setSpacing(4));
private GuiButton buttonPauseResume = new GuiButton(panel).onClick(() -> { private GuiButton buttonPauseResume = new GuiButton(panel).onClick(() -> {
if (Utils.ifMinimalModeDoPopup(panel, () -> {})) return;
if (paused) { if (paused) {
packetListener.addMarker(MarkerProcessor.MARKER_NAME_END_CUT); packetListener.addMarker(MarkerProcessor.MARKER_NAME_END_CUT);
} else { } else {
@@ -42,6 +44,7 @@ public class GuiRecordingControls extends EventRegistrations {
}).setSize(98, 20); }).setSize(98, 20);
private GuiButton buttonStartStop = new GuiButton(panel).onClick(() -> { private GuiButton buttonStartStop = new GuiButton(panel).onClick(() -> {
if (Utils.ifMinimalModeDoPopup(panel, () -> {})) return;
if (stopped) { if (stopped) {
paused = false; paused = false;
packetListener.addMarker(MarkerProcessor.MARKER_NAME_END_CUT); packetListener.addMarker(MarkerProcessor.MARKER_NAME_END_CUT);

View File

@@ -122,7 +122,7 @@ public class ConnectionEventHandler {
guiOverlay = new GuiRecordingOverlay(mc, core.getSettingsRegistry(), guiControls); guiOverlay = new GuiRecordingOverlay(mc, core.getSettingsRegistry(), guiControls);
guiOverlay.register(); guiOverlay.register();
if (core.getSettingsRegistry().get(Setting.AUTO_START_RECORDING)) { if (core.getSettingsRegistry().get(Setting.AUTO_START_RECORDING) || ReplayMod.isMinimalMode()) {
core.printInfoToChat("replaymod.chat.recordingstarted"); core.printInfoToChat("replaymod.chat.recordingstarted");
} else { } else {
packetListener.addMarker(MarkerProcessor.MARKER_NAME_START_CUT, 0); packetListener.addMarker(MarkerProcessor.MARKER_NAME_START_CUT, 0);

View File

@@ -1,7 +1,9 @@
package com.replaymod.recording.packet; package com.replaymod.recording.packet;
import com.google.gson.Gson;
import com.replaymod.core.ReplayMod; import com.replaymod.core.ReplayMod;
import com.replaymod.core.utils.Restrictions; import com.replaymod.core.utils.Restrictions;
import com.replaymod.core.versions.MCVer;
import com.replaymod.editor.gui.MarkerProcessor; import com.replaymod.editor.gui.MarkerProcessor;
import com.replaymod.recording.ReplayModRecording; import com.replaymod.recording.ReplayModRecording;
import com.replaymod.recording.Setting; import com.replaymod.recording.Setting;
@@ -45,6 +47,7 @@ import net.minecraft.network.NetworkSide;
import java.io.DataOutputStream; import java.io.DataOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashSet; import java.util.HashSet;
@@ -112,7 +115,19 @@ public class PacketListener extends ChannelInboundHandlerAdapter {
} }
try { try {
synchronized (replayFile) { synchronized (replayFile) {
replayFile.writeMetaData(metaData); if (ReplayMod.isMinimalMode()) {
metaData.setFileFormat("MCPR");
metaData.setFileFormatVersion(ReplayMetaData.CURRENT_FILE_FORMAT_VERSION);
metaData.setProtocolVersion(MCVer.getProtocolVersion());
metaData.setGenerator("ReplayMod in Minimal Mode");
try (OutputStream out = replayFile.write("metaData.json")) {
String json = (new Gson()).toJson(metaData);
out.write(json.getBytes());
}
} else {
replayFile.writeMetaData(metaData);
}
} }
} catch (IOException e) { } catch (IOException e) {
logger.error("Writing metadata:", e); logger.error("Writing metadata:", e);
@@ -197,7 +212,7 @@ public class PacketListener extends ChannelInboundHandlerAdapter {
replayFile.save(); replayFile.save();
replayFile.close(); replayFile.close();
if (core.getSettingsRegistry().get(Setting.AUTO_POST_PROCESS)) { if (core.getSettingsRegistry().get(Setting.AUTO_POST_PROCESS) && !ReplayMod.isMinimalMode()) {
MarkerProcessor.apply(outputPath, progress -> {}); MarkerProcessor.apply(outputPath, progress -> {});
} }
} catch (IOException e) { } catch (IOException e) {

View File

@@ -83,6 +83,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import static com.replaymod.core.versions.MCVer.*; import static com.replaymod.core.versions.MCVer.*;
import static com.replaymod.replaystudio.util.Utils.readInt;
/** /**
* Sends replay packets to netty channels. * Sends replay packets to netty channels.
@@ -1102,26 +1103,37 @@ public class FullReplaySender extends ChannelDuplexHandler implements ReplaySend
private final byte[] bytes; private final byte[] bytes;
PacketData(ReplayInputStream in, boolean loginPhase) throws IOException { PacketData(ReplayInputStream in, boolean loginPhase) throws IOException {
com.replaymod.replaystudio.PacketData data = in.readPacket(); if (ReplayMod.isMinimalMode()) {
timestamp = (int) data.getTime(); // Minimal mode, we can only read our exact protocol version and cannot use ReplayStudio
// We need to re-encode MCProtocolLib packets, so we can later decode them as NMS packets timestamp = readInt(in);
// The main reason we aren't reading them as NMS packets is that we want ReplayStudio to be able int length = readInt(in);
// to apply ViaVersion (and potentially other magic) to it. if (timestamp == -1 || length == -1) {
synchronized (byteBuf) { throw new EOFException();
byteBuf.markReaderIndex(); // Mark the current reader and writer index (should be at start)
byteBuf.markWriterIndex();
try {
(loginPhase ? codecLoginPhase : codecPlayPhase).encode(null, data.getPacket(), byteBuf);
} catch (Exception e) {
throw new IOException(e);
} }
bytes = new byte[length];
IOUtils.readFully(in, bytes);
} else {
com.replaymod.replaystudio.PacketData data = in.readPacket();
timestamp = (int) data.getTime();
// We need to re-encode MCProtocolLib packets, so we can later decode them as NMS packets
// The main reason we aren't reading them as NMS packets is that we want ReplayStudio to be able
// to apply ViaVersion (and potentially other magic) to it.
synchronized (byteBuf) {
byteBuf.markReaderIndex(); // Mark the current reader and writer index (should be at start)
byteBuf.markWriterIndex();
bytes = new byte[byteBuf.readableBytes()]; // Create bytes array of sufficient size try {
byteBuf.readBytes(bytes); // Read all data into bytes (loginPhase ? codecLoginPhase : codecPlayPhase).encode(null, data.getPacket(), byteBuf);
} catch (Exception e) {
throw new IOException(e);
}
byteBuf.resetReaderIndex(); // Reset reader & writer index for next use bytes = new byte[byteBuf.readableBytes()]; // Create bytes array of sufficient size
byteBuf.resetWriterIndex(); byteBuf.readBytes(bytes); // Read all data into bytes
byteBuf.resetReaderIndex(); // Reset reader & writer index for next use
byteBuf.resetWriterIndex();
}
} }
} }
} }

View File

@@ -314,6 +314,7 @@ public class ReplayHandler {
//#if MC>=10904 //#if MC>=10904
public void ensureQuickModeInitialized(Runnable andThen) { public void ensureQuickModeInitialized(Runnable andThen) {
if (Utils.ifMinimalModeDoPopup(overlay, () -> {})) return;
ListenableFuture<Void> future = quickReplaySender.getInitializationPromise(); ListenableFuture<Void> future = quickReplaySender.getInitializationPromise();
if (future == null) { if (future == null) {
InitializingQuickModePopup popup = new InitializingQuickModePopup(overlay); InitializingQuickModePopup popup = new InitializingQuickModePopup(overlay);
@@ -365,6 +366,9 @@ public class ReplayHandler {
} }
public void setQuickMode(boolean quickMode) { public void setQuickMode(boolean quickMode) {
if (ReplayMod.isMinimalMode()) {
throw new UnsupportedOperationException("Quick Mode not supported in minimal mode.");
}
if (quickMode == this.quickMode) return; if (quickMode == this.quickMode) return;
if (quickMode && fullReplaySender.isAsyncMode()) { if (quickMode && fullReplaySender.isAsyncMode()) {
// If this method is called via runLater, then it cannot switch to sync mode by itself as there might be // If this method is called via runLater, then it cannot switch to sync mode by itself as there might be

View File

@@ -411,7 +411,7 @@ public class GuiReplayViewer extends GuiScreen {
} else { } else {
server.setText(metaData.getServerName()); server.setText(metaData.getServerName());
} }
incompatible = !new ReplayStudio().isCompatible(metaData.getFileFormatVersion(), metaData.getProtocolVersion()); incompatible = !ReplayMod.isCompatible(metaData.getFileFormatVersion(), metaData.getProtocolVersion());
if (incompatible) { if (incompatible) {
version.setText("Minecraft " + metaData.getMcVersion()); version.setText("Minecraft " + metaData.getMcVersion());
} }