From ebe6da28fbf74fad2b501b875040d9d6d7c48f31 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Fri, 28 Aug 2020 16:34:09 +0200 Subject: [PATCH] Run recovery in background with progress indicator (closes #326) --- .../java/com/replaymod/core/ReplayMod.java | 2 +- .../replaymod/core/gui/RestoreReplayGui.java | 130 +++++++++++------- 2 files changed, 84 insertions(+), 48 deletions(-) diff --git a/src/main/java/com/replaymod/core/ReplayMod.java b/src/main/java/com/replaymod/core/ReplayMod.java index e23a45f9..85f5f0fa 100644 --- a/src/main/java/com/replaymod/core/ReplayMod.java +++ b/src/main/java/com/replaymod/core/ReplayMod.java @@ -403,7 +403,7 @@ public class ReplayMod implements for (File file : files) { if (file.isDirectory() && file.getName().endsWith(".mcpr.tmp")) { File origFile = new File(file.getParentFile(), Files.getNameWithoutExtension(file.getName())); - new RestoreReplayGui(GuiScreen.wrap(mc.currentScreen), origFile).display(); + new RestoreReplayGui(this, GuiScreen.wrap(mc.currentScreen), origFile).display(); } } } diff --git a/src/main/java/com/replaymod/core/gui/RestoreReplayGui.java b/src/main/java/com/replaymod/core/gui/RestoreReplayGui.java index 73b56e12..5240ac66 100644 --- a/src/main/java/com/replaymod/core/gui/RestoreReplayGui.java +++ b/src/main/java/com/replaymod/core/gui/RestoreReplayGui.java @@ -2,6 +2,8 @@ package com.replaymod.core.gui; import com.google.common.io.Files; import com.google.gson.Gson; +import com.replaymod.core.ReplayMod; +import com.replaymod.core.utils.Utils; import com.replaymod.core.versions.MCVer; import com.replaymod.replaystudio.io.ReplayInputStream; import com.replaymod.replaystudio.io.ReplayOutputStream; @@ -12,22 +14,30 @@ import com.replaymod.replaystudio.studio.ReplayStudio; import de.johni0702.minecraft.gui.container.AbstractGuiScreen; import de.johni0702.minecraft.gui.container.GuiPanel; import de.johni0702.minecraft.gui.container.GuiScreen; +import de.johni0702.minecraft.gui.container.VanillaGuiScreen; import de.johni0702.minecraft.gui.element.GuiButton; import de.johni0702.minecraft.gui.element.GuiLabel; +import de.johni0702.minecraft.gui.element.advanced.GuiProgressBar; import de.johni0702.minecraft.gui.layout.CustomLayout; import de.johni0702.minecraft.gui.layout.HorizontalLayout; import de.johni0702.minecraft.gui.layout.VerticalLayout; +import de.johni0702.minecraft.gui.utils.Colors; +import net.minecraft.util.crash.CrashReport; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import java.io.File; import java.io.IOException; import java.io.OutputStream; +import java.util.function.Consumer; import static com.replaymod.replaystudio.util.Utils.readInt; import static com.replaymod.replaystudio.util.Utils.writeInt; public class RestoreReplayGui extends AbstractGuiScreen { + private static Logger LOGGER = LogManager.getLogger(); public final GuiScreen parent; public final File file; @@ -37,8 +47,10 @@ public class RestoreReplayGui extends AbstractGuiScreen { textPanel, buttonPanel).setLayout(new VerticalLayout().setSpacing(20)); public final GuiButton yesButton = new GuiButton(buttonPanel).setSize(150, 20).setI18nLabel("gui.yes"); public final GuiButton noButton = new GuiButton(buttonPanel).setSize(150, 20).setI18nLabel("gui.no"); + private final ReplayMod core; - public RestoreReplayGui(GuiScreen parent, File file) { + public RestoreReplayGui(ReplayMod core, GuiScreen parent, File file) { + this.core = core; this.parent = parent; this.file = file; @@ -47,52 +59,7 @@ public class RestoreReplayGui extends AbstractGuiScreen { new GuiLabel().setI18nText("replaymod.gui.restorereplay2", Files.getNameWithoutExtension(file.getName())), new GuiLabel().setI18nText("replaymod.gui.restorereplay3")); yesButton.onClick(() -> { - try { - ReplayStudio studio = new ReplayStudio(); - ReplayFile replayFile = new ZipReplayFile(studio, null, file); - // Commit all not-yet-committed files into the main zip file. - // If we don't do this, then re-writing packet data below can actually overwrite uncommitted packet data! - replayFile.save(); - ReplayMetaData metaData = replayFile.getMetaData(); - if (metaData != null && metaData.getDuration() == 0) { - // Try to restore replay duration - // We need to re-write the packet data in case there are any incomplete packets dangling at the end - try (ReplayInputStream in = replayFile.getPacketData(MCVer.getPacketTypeRegistry(true)); - ReplayOutputStream out = replayFile.writePacketData()) { - 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 - 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(); - } catch (IOException e) { - e.printStackTrace(); - } + recoverInBackground(); parent.display(); }); noButton.onClick(() -> { @@ -121,4 +88,73 @@ public class RestoreReplayGui extends AbstractGuiScreen { protected RestoreReplayGui getThis() { return this; } + + private void recoverInBackground() { + GuiLabel label = new GuiLabel().setI18nText("replaymod.gui.replaysaving.title").setColor(Colors.BLACK); + GuiProgressBar progressBar = new GuiProgressBar().setHeight(14); + GuiPanel savingProcess = new GuiPanel() + .setLayout(new VerticalLayout()) + .addElements(new VerticalLayout.Data(0.5), label, progressBar); + new Thread(() -> { + core.runLater(() -> core.getBackgroundProcesses().addProcess(savingProcess)); + try { + tryRecover(progressBar::setProgress); + } catch (IOException e) { + LOGGER.error("Recovering replay file:", e); + CrashReport crashReport = CrashReport.create(e, "Recovering replay file"); + core.runLater(() -> Utils.error(LOGGER, VanillaGuiScreen.setup(getMinecraft().currentScreen), crashReport, () -> {})); + } finally { + core.runLater(() -> core.getBackgroundProcesses().removeProcess(savingProcess)); + } + }).start(); + } + + private void tryRecover(Consumer progress) throws IOException { + ReplayStudio studio = new ReplayStudio(); + ReplayFile replayFile = new ZipReplayFile(studio, null, file); + // Commit all not-yet-committed files into the main zip file. + // If we don't do this, then re-writing packet data below can actually overwrite uncommitted packet data! + replayFile.save(); + progress.accept(0.4f); + ReplayMetaData metaData = replayFile.getMetaData(); + if (metaData != null && metaData.getDuration() == 0) { + // Try to restore replay duration + // We need to re-write the packet data in case there are any incomplete packets dangling at the end + try (ReplayInputStream in = replayFile.getPacketData(MCVer.getPacketTypeRegistry(true)); + ReplayOutputStream out = replayFile.writePacketData()) { + 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 + try (OutputStream out = replayFile.write("metaData.json")) { + metaData.setGenerator(metaData.getGenerator() + "(+ ReplayMod Replay Recovery)"); + String json = (new Gson()).toJson(metaData); + out.write(json.getBytes()); + } + } + progress.accept(0.6f); + replayFile.save(); + progress.accept(0.9f); + replayFile.close(); + progress.accept(1f); + } }