From 057edccf26350a0c4b39faa309708a8a1799af14 Mon Sep 17 00:00:00 2001 From: johni0702 Date: Thu, 19 Jan 2017 17:06:29 +0100 Subject: [PATCH 1/7] Manually inject root cert for cross-signed LetsEncrypt cert in requests JREs prior to 8u101 do not ship with the root certificate that cross-signed the LetsEncrypt X3 CA used to sign server certificates. As the standard native Minecraft launcher for Windows and OSX ship with an older Java version, this commit imports the required root cert and uses it wherever communication with the replaymod.com server is established. Effectively reverts 682fb4b --- .../java/com/replaymod/core/utils/Utils.java | 48 +++++++++++++++++++ .../com/replaymod/extras/OpenEyeExtra.java | 9 +++- .../com/replaymod/online/api/ApiClient.java | 19 ++++++-- .../replaymod/online/api/SimpleApiClient.java | 9 ++-- .../online/api/replay/FileUploader.java | 7 ++- .../api/replay/ReplayModApiMethods.java | 2 +- src/main/resources/dst_root_ca_x3.pem | 20 ++++++++ 7 files changed, 102 insertions(+), 12 deletions(-) create mode 100644 src/main/resources/dst_root_ca_x3.pem diff --git a/src/main/java/com/replaymod/core/utils/Utils.java b/src/main/java/com/replaymod/core/utils/Utils.java index 2457ae15..8b27fff4 100644 --- a/src/main/java/com/replaymod/core/utils/Utils.java +++ b/src/main/java/com/replaymod/core/utils/Utils.java @@ -27,8 +27,20 @@ import org.lwjgl.util.ReadableDimension; import javax.annotation.Nullable; import javax.imageio.ImageIO; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManagerFactory; import java.awt.image.BufferedImage; import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Date; @@ -51,6 +63,42 @@ public class Utils { DEFAULT_THUMBNAIL = thumbnail; } + + /** + * Neither the root certificate of LetsEncrypt nor the root that cross-signed it is included in the default + * Java keystore prior to 8u101. + * Therefore whenever a connection to the replaymod.com site is made, this SSLContext has to be used instead. + * It has been constructed to include the necessary root certificates. + * @see #SSL_SOCKET_FACTORY + */ + public static final SSLContext SSL_CONTEXT; + + /** + * @see #SSL_CONTEXT + */ + public static final SSLSocketFactory SSL_SOCKET_FACTORY; + + static { + // Largely from https://community.letsencrypt.org/t/134/37 + try (InputStream in = Utils.class.getResourceAsStream("/dst_root_ca_x3.pem")){ + Certificate certificate = CertificateFactory.getInstance("X.509").generateCertificate(in); + + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load(null, null); + keyStore.setCertificateEntry("1", certificate); + + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(keyStore); + + SSLContext ctx = SSLContext.getInstance("TLS"); + ctx.init(null, trustManagerFactory.getTrustManagers(), null); + SSL_CONTEXT = ctx; + SSL_SOCKET_FACTORY = ctx.getSocketFactory(); + } catch (IOException | CertificateException | KeyStoreException | NoSuchAlgorithmException | KeyManagementException e) { + throw new RuntimeException(e); + } + } + public static String convertSecondsToShortString(int seconds) { int hours = seconds/(60*60); int min = seconds/60 - hours*60; diff --git a/src/main/java/com/replaymod/extras/OpenEyeExtra.java b/src/main/java/com/replaymod/extras/OpenEyeExtra.java index e198a826..ad5c1004 100644 --- a/src/main/java/com/replaymod/extras/OpenEyeExtra.java +++ b/src/main/java/com/replaymod/extras/OpenEyeExtra.java @@ -17,6 +17,7 @@ import de.johni0702.minecraft.gui.utils.Colors; import net.minecraftforge.fml.common.Loader; import org.apache.commons.io.FileUtils; +import javax.net.ssl.HttpsURLConnection; import java.io.File; import java.io.FileOutputStream; import java.net.URL; @@ -24,8 +25,10 @@ import java.nio.channels.Channels; import java.nio.channels.FileChannel; import java.nio.channels.ReadableByteChannel; +import static com.replaymod.core.utils.Utils.SSL_SOCKET_FACTORY; + public class OpenEyeExtra implements Extra { - private static final String DOWNLOAD_URL = "http://www.replaymod.com/dl/openeye/" + Loader.MC_VERSION; + private static final String DOWNLOAD_URL = "https://www.replaymod.com/dl/openeye/" + Loader.MC_VERSION; private static final Setting ASK_FOR_OPEN_EYE = new Setting<>("advanced", "askForOpenEye", null, true); private ReplayMod mod; @@ -69,7 +72,9 @@ public class OpenEyeExtra implements Extra { File targetFile = new File("mods/" + Loader.MC_VERSION, "OpenEye.jar"); FileUtils.forceMkdir(targetFile.getParentFile()); - ReadableByteChannel in = Channels.newChannel(new URL(DOWNLOAD_URL).openStream()); + HttpsURLConnection connection = (HttpsURLConnection) new URL(DOWNLOAD_URL).openConnection(); + connection.setSSLSocketFactory(SSL_SOCKET_FACTORY); + ReadableByteChannel in = Channels.newChannel(connection.getInputStream()); FileChannel out = new FileOutputStream(targetFile).getChannel(); out.transferFrom(in, 0, Long.MAX_VALUE); } catch (Throwable e) { diff --git a/src/main/java/com/replaymod/online/api/ApiClient.java b/src/main/java/com/replaymod/online/api/ApiClient.java index f8e8a1b1..a12cd822 100755 --- a/src/main/java/com/replaymod/online/api/ApiClient.java +++ b/src/main/java/com/replaymod/online/api/ApiClient.java @@ -15,13 +15,19 @@ import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import javax.imageio.ImageIO; +import javax.net.ssl.HttpsURLConnection; import java.awt.image.BufferedImage; -import java.io.*; -import java.net.HttpURLConnection; +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; import java.net.URL; import java.util.List; import java.util.function.Consumer; +import static com.replaymod.core.utils.Utils.SSL_SOCKET_FACTORY; + public class ApiClient { private static final Minecraft mc = Minecraft.getMinecraft(); @@ -130,7 +136,11 @@ public class ApiClient { QueryBuilder builder = new QueryBuilder(ReplayModApiMethods.get_thumbnail); builder.put("id", file); URL url = new URL(builder.toString()); - return ImageIO.read(url); + HttpsURLConnection connection = (HttpsURLConnection) url.openConnection(); + connection.setSSLSocketFactory(SSL_SOCKET_FACTORY); + try (InputStream in = connection.getInputStream()) { + return ImageIO.read(in); + } } private boolean cancelDownload = false; @@ -143,7 +153,8 @@ public class ApiClient { builder.put("id", file); String url = builder.toString(); URL website = new URL(url); - HttpURLConnection con = (HttpURLConnection) website.openConnection(); + HttpsURLConnection con = (HttpsURLConnection) website.openConnection(); + con.setSSLSocketFactory(SSL_SOCKET_FACTORY); int fileSize = con.getContentLength(); diff --git a/src/main/java/com/replaymod/online/api/SimpleApiClient.java b/src/main/java/com/replaymod/online/api/SimpleApiClient.java index b2871c6c..c3add7a3 100755 --- a/src/main/java/com/replaymod/online/api/SimpleApiClient.java +++ b/src/main/java/com/replaymod/online/api/SimpleApiClient.java @@ -7,12 +7,14 @@ import com.google.gson.JsonParser; import com.replaymod.online.api.replay.holders.ApiError; import org.apache.commons.io.IOUtils; +import javax.net.ssl.HttpsURLConnection; import java.io.IOException; import java.io.InputStream; -import java.net.HttpURLConnection; import java.net.URL; import java.util.Map; +import static com.replaymod.core.utils.Utils.SSL_SOCKET_FACTORY; + public class SimpleApiClient { private static final JsonParser jsonParser = new JsonParser(); @@ -72,10 +74,11 @@ public class SimpleApiClient { // read response String responseContent = null; InputStream is = null; - HttpURLConnection httpUrlConnection = null; + HttpsURLConnection httpUrlConnection = null; try { URL url = new URL(urlString); - httpUrlConnection = (HttpURLConnection) url.openConnection(); + httpUrlConnection = (HttpsURLConnection) url.openConnection(); + httpUrlConnection.setSSLSocketFactory(SSL_SOCKET_FACTORY); httpUrlConnection.setRequestMethod("GET"); diff --git a/src/main/java/com/replaymod/online/api/replay/FileUploader.java b/src/main/java/com/replaymod/online/api/replay/FileUploader.java index f369b310..8dce3faa 100755 --- a/src/main/java/com/replaymod/online/api/replay/FileUploader.java +++ b/src/main/java/com/replaymod/online/api/replay/FileUploader.java @@ -9,16 +9,18 @@ import lombok.RequiredArgsConstructor; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; +import javax.net.ssl.HttpsURLConnection; import java.io.DataOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; -import java.net.HttpURLConnection; import java.net.URL; import java.net.URLEncoder; import java.util.Set; import java.util.function.Consumer; +import static com.replaymod.core.utils.Utils.SSL_SOCKET_FACTORY; + @RequiredArgsConstructor public class FileUploader { private static final Gson gson = new Gson(); @@ -48,7 +50,8 @@ public class FileUploader { postData += "&name=" + URLEncoder.encode(filename, "UTF-8"); String url = ReplayModApiMethods.upload_file + postData; - HttpURLConnection con = (HttpURLConnection) new URL(url).openConnection(); + HttpsURLConnection con = (HttpsURLConnection) new URL(url).openConnection(); + con.setSSLSocketFactory(SSL_SOCKET_FACTORY); con.setUseCaches(false); con.setDoOutput(true); con.setRequestMethod("POST"); diff --git a/src/main/java/com/replaymod/online/api/replay/ReplayModApiMethods.java b/src/main/java/com/replaymod/online/api/replay/ReplayModApiMethods.java index 6277c1cd..524fbba1 100755 --- a/src/main/java/com/replaymod/online/api/replay/ReplayModApiMethods.java +++ b/src/main/java/com/replaymod/online/api/replay/ReplayModApiMethods.java @@ -2,7 +2,7 @@ package com.replaymod.online.api.replay; public class ReplayModApiMethods { - public static final String REPLAYMOD_BASE_URL = "http://ReplayMod.com/api/"; + public static final String REPLAYMOD_BASE_URL = "https://ReplayMod.com/api/"; public static final String register = REPLAYMOD_BASE_URL+"register"; public static final String check_authkey = REPLAYMOD_BASE_URL+"check_authkey"; diff --git a/src/main/resources/dst_root_ca_x3.pem b/src/main/resources/dst_root_ca_x3.pem new file mode 100644 index 00000000..b2e43c93 --- /dev/null +++ b/src/main/resources/dst_root_ca_x3.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/ +MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT +DkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow +PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD +Ew5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +AN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O +rz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq +OLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b +xiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw +7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD +aeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV +HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG +SIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69 +ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr +AvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz +R8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5 +JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo +Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ +-----END CERTIFICATE----- From 5302bcaed4cc2052506e30beab54e06f0d4bef14 Mon Sep 17 00:00:00 2001 From: johni0702 Date: Fri, 20 Jan 2017 16:02:42 +0100 Subject: [PATCH 2/7] Fix id and text of Replay Editor button in docs --- docs/content.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content.md b/docs/content.md index 0b3f97fe..90f9a4f5 100644 --- a/docs/content.md +++ b/docs/content.md @@ -56,7 +56,7 @@ Please use [Optifine](https://optifine.net/) instead. ### Custom Main Menu [custom-main-menu] The [Custom Main Menu](https://mods.curse.com/mc-mods/minecraft/226406-custom-main-menu) mod is often used in mod packs to customize their Main Menu with a button layout fitting the background image, links to their website / bug tracker and similar. -If you are familiar with it, the button ids for the Replay Mod are: **17890234** (text: `replaymod.gui.replayviewer`), **17890235** (text: `replaymod.gui.replayviewer`) and **17890236** (text: `replaymod.gui.replaycenter`) +If you are familiar with it, the button ids for the Replay Mod are: **17890234** (text: `replaymod.gui.replayviewer`), **17890237** (text: `replaymod.gui.replayeditor`) and **17890236** (text: `replaymod.gui.replaycenter`) Due to the nature of this Custom Main Menu mod, buttons added to the Main Menu by 3rd party mods like the **Replay Mod** will not show up by default. Thus, to access the Replay Viewer/Editor/Center, you need to manually configure the position for those buttons. From 4deb374a7f6e6cdc78a46f163dfb39dcb5e8768b Mon Sep 17 00:00:00 2001 From: johni0702 Date: Fri, 20 Jan 2017 16:16:01 +0100 Subject: [PATCH 3/7] Enable validation on focus change for all number input fields --- .../replaymod/editor/gui/GuiTrimPanel.java | 20 +++++++++++-------- .../render/gui/GuiRenderSettings.java | 6 +++--- .../gui/overlay/GuiEditMarkerPopup.java | 18 ++++++++++------- 3 files changed, 26 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/replaymod/editor/gui/GuiTrimPanel.java b/src/main/java/com/replaymod/editor/gui/GuiTrimPanel.java index ae5fd79e..fda001f1 100644 --- a/src/main/java/com/replaymod/editor/gui/GuiTrimPanel.java +++ b/src/main/java/com/replaymod/editor/gui/GuiTrimPanel.java @@ -39,6 +39,10 @@ import static com.replaymod.editor.ReplayModEditor.LOGGER; import static java.util.Optional.ofNullable; public class GuiTrimPanel extends GuiPanel { + private static GuiNumberField newGuiNumberField() { + return new GuiNumberField().setMaxLength(2).setSize(20, 20).setValidateOnFocusChange(true); + } + // Special value indicating no replay files were found private static final File NO_REPLAY = new File("."); // Special value for the initial "Select Marker Keyframe" entry @@ -50,16 +54,16 @@ public class GuiTrimPanel extends GuiPanel { .setMinSize(new Dimension(200, 20)).onSelection(i -> updateSelectedReplay()) .setToString(f -> f == NO_REPLAY ? "" : FilenameUtils.getBaseName(f.getName())); - public final GuiNumberField startHour = new GuiNumberField().setSize(20, 20).setMaxLength(2); - public final GuiNumberField startMin = new GuiNumberField().setSize(20, 20).setMaxLength(2); - public final GuiNumberField startSec = new GuiNumberField().setSize(20, 20).setMaxLength(2); - public final GuiNumberField startMilli = new GuiNumberField().setSize(40, 20).setMaxLength(4); + public final GuiNumberField startHour = newGuiNumberField(); + public final GuiNumberField startMin = newGuiNumberField(); + public final GuiNumberField startSec = newGuiNumberField(); + public final GuiNumberField startMilli = newGuiNumberField().setSize(40, 20).setMaxLength(4); public final GuiDropdownMenu startMarker = new GuiDropdownMenu<>(); - public final GuiNumberField endHour = new GuiNumberField().setSize(20, 20).setMaxLength(2); - public final GuiNumberField endMin = new GuiNumberField().setSize(20, 20).setMaxLength(2); - public final GuiNumberField endSec = new GuiNumberField().setSize(20, 20).setMaxLength(2); - public final GuiNumberField endMilli = new GuiNumberField().setSize(40, 20).setMaxLength(4); + public final GuiNumberField endHour = newGuiNumberField(); + public final GuiNumberField endMin = newGuiNumberField(); + public final GuiNumberField endSec = newGuiNumberField(); + public final GuiNumberField endMilli = newGuiNumberField().setSize(40, 20).setMaxLength(4); public final GuiDropdownMenu endMarker = new GuiDropdownMenu<>(); public final GuiPanel timePanel = new GuiPanel(this) diff --git a/src/main/java/com/replaymod/render/gui/GuiRenderSettings.java b/src/main/java/com/replaymod/render/gui/GuiRenderSettings.java index 14cd84fc..3c2faa88 100644 --- a/src/main/java/com/replaymod/render/gui/GuiRenderSettings.java +++ b/src/main/java/com/replaymod/render/gui/GuiRenderSettings.java @@ -80,8 +80,8 @@ public class GuiRenderSettings extends GuiScreen implements Closeable { } }).setMinSize(new Dimension(0, 20)).setValues(RenderSettings.EncodingPreset.values()); - public final GuiNumberField videoWidth = new GuiNumberField().setSize(50, 20).setMinValue(1); - public final GuiNumberField videoHeight = new GuiNumberField().setSize(50, 20).setMinValue(1); + public final GuiNumberField videoWidth = new GuiNumberField().setSize(50, 20).setMinValue(1).setValidateOnFocusChange(true); + public final GuiNumberField videoHeight = new GuiNumberField().setSize(50, 20).setMinValue(1).setValidateOnFocusChange(true); public final GuiSlider frameRateSlider = new GuiSlider().onValueChanged(new Runnable() { @Override public void run() { @@ -93,7 +93,7 @@ public class GuiRenderSettings extends GuiScreen implements Closeable { .setLayout(new HorizontalLayout(HorizontalLayout.Alignment.RIGHT).setSpacing(2)) .addElements(new HorizontalLayout.Data(0.5), videoWidth, new GuiLabel().setText("*"), videoHeight); - public final GuiNumberField bitRateField = new GuiNumberField().setValue(10).setSize(50, 20); + public final GuiNumberField bitRateField = new GuiNumberField().setValue(10).setSize(50, 20).setValidateOnFocusChange(true); public final GuiDropdownMenu bitRateUnit = new GuiDropdownMenu() .setSize(50, 20).setValues("bps", "kbps", "mbps").setSelected("mbps"); diff --git a/src/main/java/com/replaymod/replay/gui/overlay/GuiEditMarkerPopup.java b/src/main/java/com/replaymod/replay/gui/overlay/GuiEditMarkerPopup.java index 23e9e5b5..cc94f3cc 100644 --- a/src/main/java/com/replaymod/replay/gui/overlay/GuiEditMarkerPopup.java +++ b/src/main/java/com/replaymod/replay/gui/overlay/GuiEditMarkerPopup.java @@ -16,6 +16,10 @@ import org.lwjgl.input.Keyboard; import org.lwjgl.util.ReadablePoint; public class GuiEditMarkerPopup extends AbstractGuiPopup implements Typeable { + private static GuiNumberField newGuiNumberField() { + return new GuiNumberField().setSize(150, 20).setValidateOnFocusChange(true); + } + private final ReplayHandler replayHandler; private final Marker marker; @@ -23,15 +27,15 @@ public class GuiEditMarkerPopup extends AbstractGuiPopup imp public final GuiTextField nameField = new GuiTextField().setSize(150, 20); // TODO: Replace with a min/sec/msec field - public final GuiNumberField timeField = new GuiNumberField().setSize(150, 20).setPrecision(0); + public final GuiNumberField timeField = newGuiNumberField().setPrecision(0); - public final GuiNumberField xField = new GuiNumberField().setSize(150, 20).setPrecision(10); - public final GuiNumberField yField = new GuiNumberField().setSize(150, 20).setPrecision(10); - public final GuiNumberField zField = new GuiNumberField().setSize(150, 20).setPrecision(10); + public final GuiNumberField xField = newGuiNumberField().setPrecision(10); + public final GuiNumberField yField = newGuiNumberField().setPrecision(10); + public final GuiNumberField zField = newGuiNumberField().setPrecision(10); - public final GuiNumberField yawField = new GuiNumberField().setSize(150, 20).setPrecision(5); - public final GuiNumberField pitchField = new GuiNumberField().setSize(150, 20).setPrecision(5); - public final GuiNumberField rollField = new GuiNumberField().setSize(150, 20).setPrecision(5); + public final GuiNumberField yawField = newGuiNumberField().setPrecision(5); + public final GuiNumberField pitchField = newGuiNumberField().setPrecision(5); + public final GuiNumberField rollField = newGuiNumberField().setPrecision(5); public final GuiPanel inputs = GuiPanel.builder() .layout(new GridLayout().setColumns(2).setSpacingX(7).setSpacingY(3)) From d2def94fc17b0046ad028324f3560bb5794b28fc Mon Sep 17 00:00:00 2001 From: johni0702 Date: Mon, 23 Jan 2017 17:52:11 +0100 Subject: [PATCH 4/7] Fix duration of replays corrupted because of JVM/OS crash A JVM or OS crash can lead to incomplete packets at the end of a recording. The duration of a corrupted replay file is not restored properly if that file contains an incomplete packet. Furthermore, the entity tracker does not load properly with such an incomplete packet. Both bugs are fixed with this commit. To fix these bugs for replays that have already been recovered, simply open them in the Replay Viewer, modify them somehow (save a new path) and force the game to quit forcefully. The replays will be recovered the next time the game starts and both bugs will be fixed. --- .../java/com/replaymod/core/gui/RestoreReplayGui.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/replaymod/core/gui/RestoreReplayGui.java b/src/main/java/com/replaymod/core/gui/RestoreReplayGui.java index c6a4a3d1..d922c1e4 100644 --- a/src/main/java/com/replaymod/core/gui/RestoreReplayGui.java +++ b/src/main/java/com/replaymod/core/gui/RestoreReplayGui.java @@ -3,6 +3,7 @@ package com.replaymod.core.gui; import com.google.common.io.Files; import com.replaymod.replaystudio.PacketData; import com.replaymod.replaystudio.io.ReplayInputStream; +import com.replaymod.replaystudio.io.ReplayOutputStream; import com.replaymod.replaystudio.replay.ReplayFile; import com.replaymod.replaystudio.replay.ReplayMetaData; import com.replaymod.replaystudio.replay.ZipReplayFile; @@ -45,15 +46,19 @@ public class RestoreReplayGui extends AbstractGuiScreen { ReplayMetaData metaData = replayFile.getMetaData(); if (metaData != null && metaData.getDuration() == 0) { // Try to restore replay duration - try (ReplayInputStream in = replayFile.getPacketData()) { + // We need to re-write the packet data in case there are any incomplete packets dangling at the end + try (ReplayInputStream in = replayFile.getPacketData(); + ReplayOutputStream out = replayFile.writePacketData()) { PacketData last = null; while ((last = in.readPacket()) != null) { metaData.setDuration((int) last.getTime()); + out.write(last); } - replayFile.writeMetaData(metaData); } catch (Throwable t) { t.printStackTrace(); } + // Write back the actual duration + replayFile.writeMetaData(metaData); } replayFile.save(); replayFile.close(); From 3f2b3d62f7000bb7697954814c2049701dce729c Mon Sep 17 00:00:00 2001 From: johni0702 Date: Tue, 24 Jan 2017 09:39:12 +0100 Subject: [PATCH 5/7] Fix replacing source file in Replay Editor (fixes #57) --- .../replaymod/editor/gui/GuiReplayEditor.java | 35 +++++++++++++++---- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/replaymod/editor/gui/GuiReplayEditor.java b/src/main/java/com/replaymod/editor/gui/GuiReplayEditor.java index 98ef0760..62b027ab 100644 --- a/src/main/java/com/replaymod/editor/gui/GuiReplayEditor.java +++ b/src/main/java/com/replaymod/editor/gui/GuiReplayEditor.java @@ -1,5 +1,6 @@ package com.replaymod.editor.gui; +import com.google.common.io.Files; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.gson.JsonObject; @@ -34,6 +35,7 @@ import de.johni0702.minecraft.gui.popup.GuiInfoPopup; import de.johni0702.minecraft.gui.popup.GuiYesNoPopup; import de.johni0702.minecraft.gui.utils.Colors; import net.minecraft.crash.CrashReport; +import org.apache.commons.io.FileUtils; import org.apache.commons.io.FilenameUtils; import org.lwjgl.util.ReadableDimension; @@ -119,10 +121,21 @@ public class GuiReplayEditor extends GuiScreen { public void save(File inputFile, PacketStream.FilterInfo...filters) { save(FilenameUtils.getBaseName(inputFile.getName()), (outputFile) -> { Studio studio = new ReplayStudio(); - try (ReplayFile outputReplay = new ZipReplayFile(studio, inputFile, outputFile); - ReplayOutputStream out = outputReplay.writePacketData()) { - // The input replay file MUST be closed before saving the output file - try (ReplayFile inputReplay = new ZipReplayFile(studio, inputFile); + File tmpDir = null; + try { + File actualOutputFile = outputFile; + if (outputFile.getCanonicalPath().equals(inputFile.getCanonicalPath())) { + // Input and output files are identical. Due to the way the ZipReplayFile stores its temporary + // data, the same replay file must not be opened twice for writing (tmp files will be deleted once + // either one is closed or, on Windows, will throw an exception when deleted). + tmpDir = Files.createTempDir(); + outputFile = new File(tmpDir, "replay.mcpr"); + LOGGER.debug("Output file is identical to input file, using temporary output file {} instead", + outputFile); + } + try (ReplayFile outputReplay = new ZipReplayFile(studio, inputFile, outputFile); + ReplayOutputStream out = outputReplay.writePacketData(); + ReplayFile inputReplay = new ZipReplayFile(studio, inputFile); ReplayInputStream in = inputReplay.getPacketData()) { ReplayMetaData metaData = inputReplay.getMetaData(); PacketStream stream = studio.createReplayStream(in, true); @@ -154,11 +167,21 @@ public class GuiReplayEditor extends GuiScreen { // Update duration of new replay metaData.setDuration((int) lastTimestamp); outputReplay.writeMetaData(metaData); + + out.close(); + outputReplay.save(); + } + if (outputFile != actualOutputFile) { + LOGGER.debug("Moving temporary output file {} to {}"); + FileUtils.forceDelete(actualOutputFile); + FileUtils.moveFile(outputFile, actualOutputFile); } - out.close(); - outputReplay.save(); } catch (IOException e) { throw new RuntimeException(e); + } finally { + if (tmpDir != null && !FileUtils.deleteQuietly(tmpDir)) { + LOGGER.warn("Failed to delete temporary directory {}", tmpDir); + } } }); } From 70e3e5403885f4e885df45978cdc9fadcd9558d3 Mon Sep 17 00:00:00 2001 From: johni0702 Date: Tue, 24 Jan 2017 13:07:50 +0100 Subject: [PATCH 6/7] Fix AbstractMethodError caused by FG2.0 not supporting lambdas (fixes #58) Lambdas are not properly re-obfuscated prior to FG2.2 causing said errors when used. This is the only instance of such a lambda. FG2.2 only supports MC 1.8.8 and up as such the lambda was replaced with an anonymous inner class to solve the problem. --- .../packet/ResourcePackRecorder.java | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/replaymod/recording/packet/ResourcePackRecorder.java b/src/main/java/com/replaymod/recording/packet/ResourcePackRecorder.java index a00ff274..d4cb6c0f 100644 --- a/src/main/java/com/replaymod/recording/packet/ResourcePackRecorder.java +++ b/src/main/java/com/replaymod/recording/packet/ResourcePackRecorder.java @@ -9,6 +9,7 @@ import com.replaymod.replaystudio.replay.ReplayFile; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiScreenWorking; import net.minecraft.client.gui.GuiYesNo; +import net.minecraft.client.gui.GuiYesNoCallback; import net.minecraft.client.multiplayer.ServerData; import net.minecraft.client.multiplayer.ServerList; import net.minecraft.client.network.NetHandlerPlayClient; @@ -107,19 +108,24 @@ public class ResourcePackRecorder { } else if (serverData != null && serverData.getResourceMode() != ServerData.ServerResourceMode.PROMPT) { netManager.sendPacket(new C19PacketResourcePackStatus(hash, C19PacketResourcePackStatus.Action.DECLINED)); } else { - mc.addScheduledTask(() -> mc.displayGuiScreen(new GuiYesNo((result, id) -> { - if (serverData != null) { - serverData.setResourceMode(result ? ServerData.ServerResourceMode.ENABLED : ServerData.ServerResourceMode.DISABLED); - } - if (result) { - netManager.sendPacket(new C19PacketResourcePackStatus(hash, C19PacketResourcePackStatus.Action.ACCEPTED)); - downloadResourcePackFuture(requestId, url, hash); - } else { - netManager.sendPacket(new C19PacketResourcePackStatus(hash, C19PacketResourcePackStatus.Action.DECLINED)); - } + // Lambdas MUST NOT be used with methods that need re-obfuscation in FG prior to 2.2 (will result in AbstractMethodError) + //noinspection Convert2Lambda + mc.addScheduledTask(() -> mc.displayGuiScreen(new GuiYesNo(new GuiYesNoCallback() { + @Override + public void confirmClicked(boolean result, int id) { + if (serverData != null) { + serverData.setResourceMode(result ? ServerData.ServerResourceMode.ENABLED : ServerData.ServerResourceMode.DISABLED); + } + if (result) { + netManager.sendPacket(new C19PacketResourcePackStatus(hash, C19PacketResourcePackStatus.Action.ACCEPTED)); + ResourcePackRecorder.this.downloadResourcePackFuture(requestId, url, hash); + } else { + netManager.sendPacket(new C19PacketResourcePackStatus(hash, C19PacketResourcePackStatus.Action.DECLINED)); + } - ServerList.func_147414_b(serverData); - mc.displayGuiScreen(null); + ServerList.func_147414_b(serverData); + mc.displayGuiScreen(null); + } }, I18n.format("multiplayer.texturePrompt.line1"), I18n.format("multiplayer.texturePrompt.line2"), 0))); } } From 64898ce288f5fcc54ebb4304a9b1f0aad76413bf Mon Sep 17 00:00:00 2001 From: johni0702 Date: Tue, 24 Jan 2017 13:08:55 +0100 Subject: [PATCH 7/7] Fix NPE when saving recorded resource pack --- .../com/replaymod/recording/packet/ResourcePackRecorder.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/com/replaymod/recording/packet/ResourcePackRecorder.java b/src/main/java/com/replaymod/recording/packet/ResourcePackRecorder.java index d4cb6c0f..ed5cfc7f 100644 --- a/src/main/java/com/replaymod/recording/packet/ResourcePackRecorder.java +++ b/src/main/java/com/replaymod/recording/packet/ResourcePackRecorder.java @@ -27,6 +27,7 @@ import javax.annotation.Nonnull; import java.io.File; import java.io.IOException; import java.io.OutputStream; +import java.util.HashMap; import java.util.Map; /** @@ -53,6 +54,9 @@ public class ResourcePackRecorder { boolean doWrite = false; // Whether we are the first and have to write it synchronized (replayFile) { // Need to read, modify and write the resource pack index atomically Map index = replayFile.getResourcePackIndex(); + if (index == null) { + index = new HashMap<>(); + } if (!index.containsValue(hash)) { // Hash is unknown, we have to write the resource pack ourselves doWrite = true;