From af55951f429ff8ce329f50f4b3d68c6bee3147c3 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Thu, 20 Jun 2019 16:50:33 +0200 Subject: [PATCH] Introduce minimal mode (allows RM to run on unsupported MC versions) by disabling various version-specific features (i.e. everything which requires the ReplayStudio). --- .../java/com/replaymod/core/ReplayMod.java | 41 ++++++++++-- .../replaymod/core/gui/RestoreReplayGui.java | 35 ++++++++-- .../java/com/replaymod/core/utils/Utils.java | 64 ++++++++++++++++++- .../com/replaymod/core/versions/MCVer.java | 9 +++ .../replaymod/editor/handler/GuiHandler.java | 1 + .../replaymod/online/gui/GuiReplayCenter.java | 3 +- .../recording/gui/GuiRecordingControls.java | 3 + .../handler/ConnectionEventHandler.java | 2 +- .../recording/packet/PacketListener.java | 19 +++++- .../replaymod/replay/FullReplaySender.java | 46 ++++++++----- .../com/replaymod/replay/ReplayHandler.java | 4 ++ .../replay/gui/screen/GuiReplayViewer.java | 2 +- src/main/resources/assets/replaymod/lang | 2 +- 13 files changed, 195 insertions(+), 36 deletions(-) diff --git a/src/main/java/com/replaymod/core/ReplayMod.java b/src/main/java/com/replaymod/core/ReplayMod.java index f50e3070..d1f3875d 100755 --- a/src/main/java/com/replaymod/core/ReplayMod.java +++ b/src/main/java/com/replaymod/core/ReplayMod.java @@ -14,6 +14,7 @@ import com.replaymod.online.ReplayModOnline; import com.replaymod.recording.ReplayModRecording; import com.replaymod.render.ReplayModRender; import com.replaymod.replay.ReplayModReplay; +import com.replaymod.replaystudio.studio.ReplayStudio; import com.replaymod.replaystudio.util.I18n; import com.replaymod.simplepathing.ReplayModSimplePathing; import de.johni0702.minecraft.gui.container.GuiScreen; @@ -157,6 +158,16 @@ public class ReplayMod implements 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() { I18n.setI18n(net.minecraft.client.resource.language.I18n::translate); @@ -164,12 +175,17 @@ public class ReplayMod implements // Check Minecraft protocol version for compatibility int supportedProtocol = MinecraftConstants.PROTOCOL_VERSION; int actualProtocol = SharedConstants.getGameVersion().getProtocolVersion(); - if (supportedProtocol != actualProtocol) { - 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() - )); + if (supportedProtocol != actualProtocol && !Boolean.parseBoolean(System.getProperty("replaymod.skipversioncheck", "false"))) { + minimalMode = true; + // Only allow use of older RM on newer (e.g. snapshot, pre-release) versions. + // The other way around is almost certainly user error. + 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 @@ -602,4 +618,17 @@ public class ReplayMod implements public GuiBackgroundProcesses getBackgroundProcesses() { 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); + } + } } diff --git a/src/main/java/com/replaymod/core/gui/RestoreReplayGui.java b/src/main/java/com/replaymod/core/gui/RestoreReplayGui.java index c02f7cbd..d15b7af0 100644 --- a/src/main/java/com/replaymod/core/gui/RestoreReplayGui.java +++ b/src/main/java/com/replaymod/core/gui/RestoreReplayGui.java @@ -1,7 +1,7 @@ package com.replaymod.core.gui; 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.ReplayOutputStream; 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.VerticalLayout; import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; import java.io.File; 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 { @@ -53,16 +58,34 @@ public class RestoreReplayGui extends AbstractGuiScreen { // 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); ReplayOutputStream out = replayFile.writePacketData(true)) { - PacketData last = null; - while ((last = in.readPacket()) != null) { - metaData.setDuration((int) last.getTime()); - out.write(last); + while (true) { + // To prevent failing at un-parsable packets and to support recovery in minimal mode, + // we do not use the ReplayIn/OutputStream methods but instead parse the packets ourselves. + 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) { t.printStackTrace(); } // 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.close(); diff --git a/src/main/java/com/replaymod/core/utils/Utils.java b/src/main/java/com/replaymod/core/utils/Utils.java index fd969e68..0a57ca59 100644 --- a/src/main/java/com/replaymod/core/utils/Utils.java +++ b/src/main/java/com/replaymod/core/utils/Utils.java @@ -5,6 +5,7 @@ import com.google.common.net.PercentEscaper; 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 de.johni0702.minecraft.gui.GuiRenderer; import de.johni0702.minecraft.gui.RenderInfo; import de.johni0702.minecraft.gui.container.AbstractGuiScrollable; @@ -26,6 +27,7 @@ import net.minecraft.util.crash.CrashReport; import net.minecraft.util.Identifier; import org.apache.commons.io.Charsets; import org.apache.commons.io.FilenameUtils; +import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; //#if MC>=11300 @@ -34,10 +36,11 @@ import org.apache.logging.log4j.Logger; //#endif //#if MC>=10800 +import com.github.steveice10.mc.protocol.MinecraftConstants; import net.minecraft.client.network.PlayerListEntry; import net.minecraft.client.util.DefaultSkinHelper; - //#else +//$$ import com.github.steveice10.mc.protocol.ProtocolConstants; //$$ import net.minecraft.client.Minecraft; //$$ import net.minecraft.client.entity.AbstractClientPlayer; //$$ import net.minecraft.entity.player.EntityPlayer; @@ -71,6 +74,7 @@ import java.util.function.Consumer; import static com.replaymod.core.versions.MCVer.getMinecraft; public class Utils { + private static Logger LOGGER = LogManager.getLogger(); 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?) @@ -315,4 +319,62 @@ public class Utils { 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() { + @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(); + } + } } diff --git a/src/main/java/com/replaymod/core/versions/MCVer.java b/src/main/java/com/replaymod/core/versions/MCVer.java index 3cccfd68..a32e03af 100644 --- a/src/main/java/com/replaymod/core/versions/MCVer.java +++ b/src/main/java/com/replaymod/core/versions/MCVer.java @@ -78,6 +78,7 @@ import net.minecraft.client.render.VertexFormatElement; //#if MC>=11400 import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.SharedConstants; //#else //#if MC>=11300 //$$ import net.minecraftforge.fml.ModList; @@ -117,6 +118,14 @@ public class MCVer { //#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 callable) { //#if MC>=10904 //#if MC>=11200 diff --git a/src/main/java/com/replaymod/editor/handler/GuiHandler.java b/src/main/java/com/replaymod/editor/handler/GuiHandler.java index 4390deb9..0d8589a2 100644 --- a/src/main/java/com/replaymod/editor/handler/GuiHandler.java +++ b/src/main/java/com/replaymod/editor/handler/GuiHandler.java @@ -41,6 +41,7 @@ public class GuiHandler extends EventRegistrations { // Inject Edit button if (!replayViewer.editorButton.getChildren().isEmpty()) return; replayViewer.replaySpecificButtons.add(new GuiButton(replayViewer.editorButton).onClick(() -> { + if (Utils.ifMinimalModeDoPopup(replayViewer, () -> {})) return; try { new GuiEditReplay(replayViewer, replayViewer.list.getSelected().file.toPath()) { @Override diff --git a/src/main/java/com/replaymod/online/gui/GuiReplayCenter.java b/src/main/java/com/replaymod/online/gui/GuiReplayCenter.java index 08c6468c..27826010 100644 --- a/src/main/java/com/replaymod/online/gui/GuiReplayCenter.java +++ b/src/main/java/com/replaymod/online/gui/GuiReplayCenter.java @@ -5,6 +5,7 @@ import com.google.common.base.Supplier; import com.google.common.primitives.Ints; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; +import com.replaymod.core.ReplayMod; import com.replaymod.core.utils.Utils; import com.replaymod.online.ReplayModOnline; import com.replaymod.online.api.ApiClient; @@ -425,7 +426,7 @@ public class GuiReplayCenter extends GuiScreen { } else { 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) { version.setText("Minecraft " + fileInfo.getMetadata().getMcVersion()); } diff --git a/src/main/java/com/replaymod/recording/gui/GuiRecordingControls.java b/src/main/java/com/replaymod/recording/gui/GuiRecordingControls.java index 23337cc7..34af6d16 100644 --- a/src/main/java/com/replaymod/recording/gui/GuiRecordingControls.java +++ b/src/main/java/com/replaymod/recording/gui/GuiRecordingControls.java @@ -1,6 +1,7 @@ package com.replaymod.recording.gui; import com.replaymod.core.ReplayMod; +import com.replaymod.core.utils.Utils; import com.replaymod.editor.gui.MarkerProcessor; import com.replaymod.recording.Setting; 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 GuiButton buttonPauseResume = new GuiButton(panel).onClick(() -> { + if (Utils.ifMinimalModeDoPopup(panel, () -> {})) return; if (paused) { packetListener.addMarker(MarkerProcessor.MARKER_NAME_END_CUT); } else { @@ -42,6 +44,7 @@ public class GuiRecordingControls extends EventRegistrations { }).setSize(98, 20); private GuiButton buttonStartStop = new GuiButton(panel).onClick(() -> { + if (Utils.ifMinimalModeDoPopup(panel, () -> {})) return; if (stopped) { paused = false; packetListener.addMarker(MarkerProcessor.MARKER_NAME_END_CUT); diff --git a/src/main/java/com/replaymod/recording/handler/ConnectionEventHandler.java b/src/main/java/com/replaymod/recording/handler/ConnectionEventHandler.java index 504ee553..67d163e4 100755 --- a/src/main/java/com/replaymod/recording/handler/ConnectionEventHandler.java +++ b/src/main/java/com/replaymod/recording/handler/ConnectionEventHandler.java @@ -122,7 +122,7 @@ public class ConnectionEventHandler { guiOverlay = new GuiRecordingOverlay(mc, core.getSettingsRegistry(), guiControls); 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"); } else { packetListener.addMarker(MarkerProcessor.MARKER_NAME_START_CUT, 0); diff --git a/src/main/java/com/replaymod/recording/packet/PacketListener.java b/src/main/java/com/replaymod/recording/packet/PacketListener.java index 3bcc7ab5..cf5d9d89 100755 --- a/src/main/java/com/replaymod/recording/packet/PacketListener.java +++ b/src/main/java/com/replaymod/recording/packet/PacketListener.java @@ -1,7 +1,9 @@ package com.replaymod.recording.packet; +import com.google.gson.Gson; import com.replaymod.core.ReplayMod; import com.replaymod.core.utils.Restrictions; +import com.replaymod.core.versions.MCVer; import com.replaymod.editor.gui.MarkerProcessor; import com.replaymod.recording.ReplayModRecording; import com.replaymod.recording.Setting; @@ -45,6 +47,7 @@ import net.minecraft.network.NetworkSide; import java.io.DataOutputStream; import java.io.IOException; +import java.io.OutputStream; import java.nio.file.Path; import java.util.Arrays; import java.util.HashSet; @@ -112,7 +115,19 @@ public class PacketListener extends ChannelInboundHandlerAdapter { } try { 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) { logger.error("Writing metadata:", e); @@ -197,7 +212,7 @@ public class PacketListener extends ChannelInboundHandlerAdapter { replayFile.save(); replayFile.close(); - if (core.getSettingsRegistry().get(Setting.AUTO_POST_PROCESS)) { + if (core.getSettingsRegistry().get(Setting.AUTO_POST_PROCESS) && !ReplayMod.isMinimalMode()) { MarkerProcessor.apply(outputPath, progress -> {}); } } catch (IOException e) { diff --git a/src/main/java/com/replaymod/replay/FullReplaySender.java b/src/main/java/com/replaymod/replay/FullReplaySender.java index 88a6cfa6..007237d4 100755 --- a/src/main/java/com/replaymod/replay/FullReplaySender.java +++ b/src/main/java/com/replaymod/replay/FullReplaySender.java @@ -83,6 +83,7 @@ import java.util.List; import java.util.Map; import static com.replaymod.core.versions.MCVer.*; +import static com.replaymod.replaystudio.util.Utils.readInt; /** * Sends replay packets to netty channels. @@ -1102,26 +1103,37 @@ public class FullReplaySender extends ChannelDuplexHandler implements ReplaySend private final byte[] bytes; PacketData(ReplayInputStream in, boolean loginPhase) throws IOException { - 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(); - - try { - (loginPhase ? codecLoginPhase : codecPlayPhase).encode(null, data.getPacket(), byteBuf); - } catch (Exception e) { - throw new IOException(e); + if (ReplayMod.isMinimalMode()) { + // Minimal mode, we can only read our exact protocol version and cannot use ReplayStudio + timestamp = readInt(in); + int length = readInt(in); + if (timestamp == -1 || length == -1) { + throw new EOFException(); } + 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 - byteBuf.readBytes(bytes); // Read all data into bytes + try { + (loginPhase ? codecLoginPhase : codecPlayPhase).encode(null, data.getPacket(), byteBuf); + } catch (Exception e) { + throw new IOException(e); + } - byteBuf.resetReaderIndex(); // Reset reader & writer index for next use - byteBuf.resetWriterIndex(); + bytes = new byte[byteBuf.readableBytes()]; // Create bytes array of sufficient size + byteBuf.readBytes(bytes); // Read all data into bytes + + byteBuf.resetReaderIndex(); // Reset reader & writer index for next use + byteBuf.resetWriterIndex(); + } } } } diff --git a/src/main/java/com/replaymod/replay/ReplayHandler.java b/src/main/java/com/replaymod/replay/ReplayHandler.java index b8365844..f87af7ef 100755 --- a/src/main/java/com/replaymod/replay/ReplayHandler.java +++ b/src/main/java/com/replaymod/replay/ReplayHandler.java @@ -314,6 +314,7 @@ public class ReplayHandler { //#if MC>=10904 public void ensureQuickModeInitialized(Runnable andThen) { + if (Utils.ifMinimalModeDoPopup(overlay, () -> {})) return; ListenableFuture future = quickReplaySender.getInitializationPromise(); if (future == null) { InitializingQuickModePopup popup = new InitializingQuickModePopup(overlay); @@ -365,6 +366,9 @@ public class ReplayHandler { } 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 && fullReplaySender.isAsyncMode()) { // If this method is called via runLater, then it cannot switch to sync mode by itself as there might be diff --git a/src/main/java/com/replaymod/replay/gui/screen/GuiReplayViewer.java b/src/main/java/com/replaymod/replay/gui/screen/GuiReplayViewer.java index 76bb3b1e..b6d3311c 100755 --- a/src/main/java/com/replaymod/replay/gui/screen/GuiReplayViewer.java +++ b/src/main/java/com/replaymod/replay/gui/screen/GuiReplayViewer.java @@ -411,7 +411,7 @@ public class GuiReplayViewer extends GuiScreen { } else { server.setText(metaData.getServerName()); } - incompatible = !new ReplayStudio().isCompatible(metaData.getFileFormatVersion(), metaData.getProtocolVersion()); + incompatible = !ReplayMod.isCompatible(metaData.getFileFormatVersion(), metaData.getProtocolVersion()); if (incompatible) { version.setText("Minecraft " + metaData.getMcVersion()); } diff --git a/src/main/resources/assets/replaymod/lang b/src/main/resources/assets/replaymod/lang index 874fd801..8936798b 160000 --- a/src/main/resources/assets/replaymod/lang +++ b/src/main/resources/assets/replaymod/lang @@ -1 +1 @@ -Subproject commit 874fd8016db8209e6a4e8822c435c9641bcc18f6 +Subproject commit 8936798bea45b8ab9422040420e8ec59aabf055d