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:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<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
|
||||
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();
|
||||
|
||||
@@ -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<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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String> callable) {
|
||||
//#if MC>=10904
|
||||
//#if MC>=11200
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -314,6 +314,7 @@ public class ReplayHandler {
|
||||
|
||||
//#if MC>=10904
|
||||
public void ensureQuickModeInitialized(Runnable andThen) {
|
||||
if (Utils.ifMinimalModeDoPopup(overlay, () -> {})) return;
|
||||
ListenableFuture<Void> 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
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user