From 136761b4249ad4a5985a258ea1fafa4796b507a0 Mon Sep 17 00:00:00 2001 From: johni0702 Date: Sun, 10 May 2015 12:31:01 +0200 Subject: [PATCH] Clean up and restructure the replay sender Add a ReplayFile class for easy reading of replay files --- .../eu/crushedpixel/replaymod/ReplayMod.java | 3 +- .../replaymod/gui/online/GuiUploadFile.java | 34 +- .../gui/replayviewer/GuiRenameReplay.java | 4 +- .../gui/replayviewer/GuiReplayViewer.java | 39 +- .../recording/ConnectionEventHandler.java | 6 +- .../replaymod/recording/DataListener.java | 3 +- .../replaymod/replay/ReplayHandler.java | 37 +- .../replaymod/replay/ReplayProcess.java | 10 +- .../replaymod/replay/ReplaySender.java | 948 +++++++++++------- .../replaymod/utils/ReplayFile.java | 117 +++ .../replaymod/utils/ReplayFileIO.java | 74 +- 11 files changed, 764 insertions(+), 511 deletions(-) create mode 100644 src/main/java/eu/crushedpixel/replaymod/utils/ReplayFile.java diff --git a/src/main/java/eu/crushedpixel/replaymod/ReplayMod.java b/src/main/java/eu/crushedpixel/replaymod/ReplayMod.java index 99c70ff3..4d7d7be8 100755 --- a/src/main/java/eu/crushedpixel/replaymod/ReplayMod.java +++ b/src/main/java/eu/crushedpixel/replaymod/ReplayMod.java @@ -13,6 +13,7 @@ import eu.crushedpixel.replaymod.registry.UploadedFileHandler; import eu.crushedpixel.replaymod.renderer.SafeEntityRenderer; import eu.crushedpixel.replaymod.replay.ReplaySender; import eu.crushedpixel.replaymod.settings.ReplaySettings; +import eu.crushedpixel.replaymod.utils.ReplayFile; import eu.crushedpixel.replaymod.utils.ReplayFileIO; import net.minecraft.client.Minecraft; import net.minecraftforge.common.MinecraftForge; @@ -158,7 +159,7 @@ public class ReplayMod { File folder = ReplayFileIO.getReplayFolder(); for(File f : folder.listFiles()) { - if(("." + FilenameUtils.getExtension(f.getAbsolutePath())).equals(ConnectionEventHandler.TEMP_FILE_EXTENSION)) { + if(("." + FilenameUtils.getExtension(f.getAbsolutePath())).equals(ReplayFile.TEMP_FILE_EXTENSION)) { f.delete(); } } diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/online/GuiUploadFile.java b/src/main/java/eu/crushedpixel/replaymod/gui/online/GuiUploadFile.java index d0ebe995..06d5ae69 100755 --- a/src/main/java/eu/crushedpixel/replaymod/gui/online/GuiUploadFile.java +++ b/src/main/java/eu/crushedpixel/replaymod/gui/online/GuiUploadFile.java @@ -7,10 +7,10 @@ import eu.crushedpixel.replaymod.api.client.holders.Category; import eu.crushedpixel.replaymod.gui.GuiConstants; import eu.crushedpixel.replaymod.gui.replayviewer.GuiReplayViewer; import eu.crushedpixel.replaymod.online.authentication.AuthenticationHandler; -import eu.crushedpixel.replaymod.recording.ConnectionEventHandler; import eu.crushedpixel.replaymod.recording.ReplayMetaData; import eu.crushedpixel.replaymod.reflection.MCPNames; import eu.crushedpixel.replaymod.utils.ImageUtils; +import eu.crushedpixel.replaymod.utils.ReplayFile; import eu.crushedpixel.replaymod.utils.ResourceHelper; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.Gui; @@ -20,8 +20,6 @@ import net.minecraft.client.gui.GuiTextField; import net.minecraft.client.renderer.texture.DynamicTexture; import net.minecraft.client.resources.I18n; import net.minecraft.util.ResourceLocation; -import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; -import org.apache.commons.compress.archivers.zip.ZipFile; import org.apache.commons.io.FilenameUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -30,7 +28,8 @@ import org.lwjgl.input.Keyboard; import javax.imageio.ImageIO; import java.awt.*; import java.awt.image.BufferedImage; -import java.io.*; +import java.io.File; +import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; @@ -64,30 +63,17 @@ public class GuiUploadFile extends GuiScreen { boolean correctFile = false; this.replayFile = file; - if(("." + FilenameUtils.getExtension(file.getAbsolutePath())).equals(ConnectionEventHandler.ZIP_FILE_EXTENSION)) { - ZipFile archive = null; + if(("." + FilenameUtils.getExtension(file.getAbsolutePath())).equals(ReplayFile.ZIP_FILE_EXTENSION)) { + ReplayFile archive = null; try { - archive = new ZipFile(file); - ZipArchiveEntry recfile = archive.getEntry("recording" + ConnectionEventHandler.TEMP_FILE_EXTENSION); - ZipArchiveEntry metadata = archive.getEntry("metaData" + ConnectionEventHandler.JSON_FILE_EXTENSION); + archive = new ReplayFile(file); - ZipArchiveEntry image = archive.getEntry("thumb"); - BufferedImage img = null; - if(image != null) { - InputStream is = archive.getInputStream(image); - is.skip(7); - BufferedImage bimg = ImageIO.read(is); - if(bimg != null) { - thumb = ImageUtils.scaleImage(bimg, new Dimension(1280, 720)); - } + metaData = archive.metadata().get(); + BufferedImage img = archive.thumb().get(); + if(img != null) { + thumb = ImageUtils.scaleImage(img, new Dimension(1280, 720)); } - InputStream is = archive.getInputStream(metadata); - BufferedReader br = new BufferedReader(new InputStreamReader(is)); - String json = br.readLine(); - - metaData = gson.fromJson(json, ReplayMetaData.class); - archive.close(); correctFile = true; } catch(Exception e) { diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/replayviewer/GuiRenameReplay.java b/src/main/java/eu/crushedpixel/replaymod/gui/replayviewer/GuiRenameReplay.java index 3458b573..2c1d27c1 100755 --- a/src/main/java/eu/crushedpixel/replaymod/gui/replayviewer/GuiRenameReplay.java +++ b/src/main/java/eu/crushedpixel/replaymod/gui/replayviewer/GuiRenameReplay.java @@ -1,6 +1,6 @@ package eu.crushedpixel.replaymod.gui.replayviewer; -import eu.crushedpixel.replaymod.recording.ConnectionEventHandler; +import eu.crushedpixel.replaymod.utils.ReplayFile; import eu.crushedpixel.replaymod.utils.ReplayFileIO; import net.minecraft.client.gui.GuiButton; import net.minecraft.client.gui.GuiScreen; @@ -48,7 +48,7 @@ public class GuiRenameReplay extends GuiScreen { } else if(button.id == 0) { File folder = ReplayFileIO.getReplayFolder(); - File initRenamed = new File(folder, this.field_146583_f.getText().trim() + ConnectionEventHandler.ZIP_FILE_EXTENSION.replaceAll("[^a-zA-Z0-9\\.\\-]", "_")); + File initRenamed = new File(folder, (this.field_146583_f.getText().trim() + ReplayFile.ZIP_FILE_EXTENSION).replaceAll("[^a-zA-Z0-9\\.\\-]", "_")); File renamed = initRenamed; int i = 1; while(renamed.isFile()) { diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/replayviewer/GuiReplayViewer.java b/src/main/java/eu/crushedpixel/replaymod/gui/replayviewer/GuiReplayViewer.java index ce9518f5..0905170d 100755 --- a/src/main/java/eu/crushedpixel/replaymod/gui/replayviewer/GuiReplayViewer.java +++ b/src/main/java/eu/crushedpixel/replaymod/gui/replayviewer/GuiReplayViewer.java @@ -1,6 +1,5 @@ package eu.crushedpixel.replaymod.gui.replayviewer; -import com.google.gson.Gson; import com.mojang.realmsclient.util.Pair; import eu.crushedpixel.replaymod.ReplayMod; import eu.crushedpixel.replaymod.api.client.holders.FileInfo; @@ -8,11 +7,11 @@ import eu.crushedpixel.replaymod.gui.GuiReplaySettings; import eu.crushedpixel.replaymod.gui.elements.GuiReplayListExtended; import eu.crushedpixel.replaymod.gui.online.GuiUploadFile; import eu.crushedpixel.replaymod.online.authentication.AuthenticationHandler; -import eu.crushedpixel.replaymod.recording.ConnectionEventHandler; import eu.crushedpixel.replaymod.recording.ReplayMetaData; import eu.crushedpixel.replaymod.replay.ReplayHandler; import eu.crushedpixel.replaymod.utils.ImageUtils; import eu.crushedpixel.replaymod.utils.MouseUtils; +import eu.crushedpixel.replaymod.utils.ReplayFile; import eu.crushedpixel.replaymod.utils.ReplayFileIO; import net.minecraft.client.gui.GuiButton; import net.minecraft.client.gui.GuiScreen; @@ -20,8 +19,6 @@ import net.minecraft.client.gui.GuiYesNo; import net.minecraft.client.gui.GuiYesNoCallback; import net.minecraft.client.resources.I18n; import net.minecraft.util.Util; -import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; -import org.apache.commons.compress.archivers.zip.ZipFile; import org.apache.commons.io.FilenameUtils; import org.lwjgl.Sys; import org.lwjgl.input.Keyboard; @@ -29,7 +26,8 @@ import org.lwjgl.input.Keyboard; import javax.imageio.ImageIO; import java.awt.*; import java.awt.image.BufferedImage; -import java.io.*; +import java.io.File; +import java.io.IOException; import java.net.URI; import java.util.*; import java.util.List; @@ -43,7 +41,6 @@ public class GuiReplayViewer extends GuiScreen implements GuiYesNoCallback { private static final int DELETE_BUTTON_ID = 9005; private static final int SETTINGS_BUTTON_ID = 9006; private static final int CANCEL_BUTTON_ID = 9007; - private static Gson gson = new Gson(); private boolean initialized; private GuiReplayListExtended replayGuiList; private List, File>> replayFileList = new ArrayList, File>>(); @@ -65,39 +62,21 @@ public class GuiReplayViewer extends GuiScreen implements GuiYesNoCallback { for(File file : ReplayFileIO.getAllReplayFiles()) { try { - ZipFile archive = new ZipFile(file); - ZipArchiveEntry metadata = archive.getEntry("metaData" + ConnectionEventHandler.JSON_FILE_EXTENSION); - - ZipArchiveEntry image = archive.getEntry("thumb"); - BufferedImage img = null; - if(image != null) { - InputStream is = archive.getInputStream(image); - is.skip(7); - BufferedImage bimg = ImageIO.read(is); - if(bimg != null) { - img = ImageUtils.scaleImage(bimg, new Dimension(1280, 720)); - } - } - + ReplayFile replayFile = new ReplayFile(file); + ReplayMetaData metaData = replayFile.metadata().get(); + BufferedImage img = replayFile.thumb().get(); File tmp = null; if(img != null) { + img = ImageUtils.scaleImage(img, new Dimension(1280, 720)); tmp = File.createTempFile(FilenameUtils.getBaseName(file.getAbsolutePath()), "jpg"); tmp.deleteOnExit(); ImageIO.write(img, "jpg", tmp); } - - InputStream is = archive.getInputStream(metadata); - BufferedReader br = new BufferedReader(new InputStreamReader(is)); - - String json = br.readLine(); - - ReplayMetaData metaData = gson.fromJson(json, ReplayMetaData.class); - replayFileList.add(Pair.of(Pair.of(file, metaData), tmp)); - - archive.close(); + replayFile.close(); } catch(Exception e) { + e.printStackTrace(); } } diff --git a/src/main/java/eu/crushedpixel/replaymod/recording/ConnectionEventHandler.java b/src/main/java/eu/crushedpixel/replaymod/recording/ConnectionEventHandler.java index 7d28fa8b..8e3ee450 100755 --- a/src/main/java/eu/crushedpixel/replaymod/recording/ConnectionEventHandler.java +++ b/src/main/java/eu/crushedpixel/replaymod/recording/ConnectionEventHandler.java @@ -2,6 +2,7 @@ package eu.crushedpixel.replaymod.recording; import eu.crushedpixel.replaymod.ReplayMod; import eu.crushedpixel.replaymod.chat.ChatMessageHandler.ChatMessageType; +import eu.crushedpixel.replaymod.utils.ReplayFile; import eu.crushedpixel.replaymod.utils.ReplayFileIO; import io.netty.channel.Channel; import io.netty.channel.ChannelHandler; @@ -26,9 +27,6 @@ import java.util.Map.Entry; public class ConnectionEventHandler { - public static final String TEMP_FILE_EXTENSION = ".tmcpr"; - public static final String JSON_FILE_EXTENSION = ".json"; - public static final String ZIP_FILE_EXTENSION = ".mcpr"; private static final String decoderKey = "decoder"; private static final String packetHandlerKey = "packet_handler"; private static final String DATE_FORMAT = "yyyy_MM_dd_HH_mm_ss"; @@ -92,7 +90,7 @@ public class ConnectionEventHandler { File folder = ReplayFileIO.getReplayFolder(); fileName = sdf.format(Calendar.getInstance().getTime()); - currentFile = new File(folder, fileName + TEMP_FILE_EXTENSION); + currentFile = new File(folder, fileName + ReplayFile.TEMP_FILE_EXTENSION); currentFile.createNewFile(); diff --git a/src/main/java/eu/crushedpixel/replaymod/recording/DataListener.java b/src/main/java/eu/crushedpixel/replaymod/recording/DataListener.java index 24733fe2..06b80519 100755 --- a/src/main/java/eu/crushedpixel/replaymod/recording/DataListener.java +++ b/src/main/java/eu/crushedpixel/replaymod/recording/DataListener.java @@ -3,6 +3,7 @@ package eu.crushedpixel.replaymod.recording; import com.google.gson.Gson; import eu.crushedpixel.replaymod.gui.GuiReplaySaving; import eu.crushedpixel.replaymod.holders.PacketData; +import eu.crushedpixel.replaymod.utils.ReplayFile; import eu.crushedpixel.replaymod.utils.ReplayFileIO; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; @@ -134,7 +135,7 @@ public abstract class DataListener extends ChannelInboundHandlerAdapter { File folder = ReplayFileIO.getReplayFolder(); - File archive = new File(folder, name + ConnectionEventHandler.ZIP_FILE_EXTENSION); + File archive = new File(folder, name + ReplayFile.ZIP_FILE_EXTENSION); archive.createNewFile(); ReplayFileIO.writeReplayFile(archive, file, metaData); diff --git a/src/main/java/eu/crushedpixel/replaymod/replay/ReplayHandler.java b/src/main/java/eu/crushedpixel/replaymod/replay/ReplayHandler.java index 5b747d2e..4b68ec77 100755 --- a/src/main/java/eu/crushedpixel/replaymod/replay/ReplayHandler.java +++ b/src/main/java/eu/crushedpixel/replaymod/replay/ReplayHandler.java @@ -4,6 +4,7 @@ import com.mojang.authlib.GameProfile; import eu.crushedpixel.replaymod.ReplayMod; import eu.crushedpixel.replaymod.entities.CameraEntity; import eu.crushedpixel.replaymod.holders.*; +import eu.crushedpixel.replaymod.utils.ReplayFile; import eu.crushedpixel.replaymod.utils.ReplayFileIO; import io.netty.channel.embedded.EmbeddedChannel; import net.minecraft.client.Minecraft; @@ -14,6 +15,7 @@ import net.minecraft.network.NetworkManager; import net.minecraft.network.play.INetHandlerPlayClient; import java.io.File; +import java.io.IOException; import java.util.*; public class ReplayHandler { @@ -36,6 +38,11 @@ public class ReplayHandler { private static KeyframeSet[] keyframeRepository = new KeyframeSet[]{}; + /** + * The file currently being played. + */ + private static ReplayFile currentReplayFile; + public static KeyframeSet[] getKeyframeRepository() { return keyframeRepository; } @@ -49,7 +56,7 @@ public class ReplayHandler { ReplayFileIO.writeKeyframeRegistryToFile(repo, tempFile); - ReplayMod.replayFileAppender.registerModifiedFile(tempFile, "paths", ReplayMod.replaySender.getReplayFile()); + ReplayMod.replayFileAppender.registerModifiedFile(tempFile, "paths", getReplayFile()); } catch(Exception e) { e.printStackTrace(); } @@ -325,13 +332,23 @@ public class ReplayHandler { channel = new OpenEmbeddedChannel(networkManager); - ReplayMod.replaySender = new ReplaySender(file, networkManager); + // Open replay + try { + currentReplayFile = new ReplayFile(file); + } catch(Exception e) { + throw new RuntimeException(e); + } + + KeyframeSet[] paths = currentReplayFile.paths().get(); + ReplayHandler.setKeyframeRepository(paths == null ? new KeyframeSet[0] : paths, false); + + ReplayMod.replaySender = new ReplaySender(currentReplayFile, true); channel.pipeline().addFirst(ReplayMod.replaySender); channel.pipeline().fireChannelActive(); try { ReplayMod.overlay.resetUI(true); - } catch(Exception e) {} + } catch(Exception e) {} // TODO proper handling //Load lighting and trigger update ReplayMod.replaySettings.setLightingEnabled(ReplayMod.replaySettings.isLightingEnabled()); @@ -374,6 +391,15 @@ public class ReplayHandler { ReplayMod.replaySender.terminateReplay(); } + if (currentReplayFile != null) { + try { + currentReplayFile.close(); + } catch (IOException e) { + e.printStackTrace(); + } + currentReplayFile = null; + } + resetKeyframes(); inReplay = false; @@ -400,10 +426,7 @@ public class ReplayHandler { } public static File getReplayFile() { - if(ReplayMod.replaySender != null) { - return ReplayMod.replaySender.getReplayFile(); - } - return null; + return currentReplayFile == null ? null : currentReplayFile.getFile(); } public static TimeKeyframe getFirstTimeKeyframe() { diff --git a/src/main/java/eu/crushedpixel/replaymod/replay/ReplayProcess.java b/src/main/java/eu/crushedpixel/replaymod/replay/ReplayProcess.java index 0a493796..9cfa5cc8 100755 --- a/src/main/java/eu/crushedpixel/replaymod/replay/ReplayProcess.java +++ b/src/main/java/eu/crushedpixel/replaymod/replay/ReplayProcess.java @@ -65,8 +65,6 @@ public class ReplayProcess { calculated = false; requestFinish = false; - ReplayMod.replaySender.resetToleratedTimeStamp(); - blocked = deepBlock = false; startRealTime = System.currentTimeMillis(); @@ -97,6 +95,7 @@ public class ReplayProcess { ReplayHandler.sortKeyframes(); ReplayHandler.setInPath(true); + ReplayMod.replaySender.setAsyncMode(false); //gets first Timestamp and sets Replay Time to it TimeKeyframe tf = ReplayHandler.getFirstTimeKeyframe(); @@ -105,7 +104,7 @@ public class ReplayProcess { if(ts < ReplayMod.replaySender.currentTimeStamp()) { mc.displayGuiScreen(null); } - ReplayMod.replaySender.jumpToTime(ts); + ReplayMod.replaySender.sendPacketsTill(ts); } ReplayMod.chatMessageHandler.addLocalizedChatMessage("replaymod.chat.pathstarted", ChatMessageType.INFORMATION); @@ -132,6 +131,7 @@ public class ReplayProcess { } ReplayHandler.setInPath(false); + ReplayMod.replaySender.setAsyncMode(true); ReplayMod.replaySender.stopHurrying(); @@ -357,8 +357,8 @@ public class ReplayProcess { lastRenderPartialTicks = MCTimerHandler.getRenderTicks(); lastTicks = MCTimerHandler.getTicks(); - if(curTimestamp != null && curTimestamp != ReplayMod.replaySender.getDesiredTimestamp()) - ReplayMod.replaySender.jumpToTime(curTimestamp); + if(curTimestamp != null) + ReplayMod.replaySender.sendPacketsTill(curTimestamp); //splinePos = (index of last entry + add) / total entries diff --git a/src/main/java/eu/crushedpixel/replaymod/replay/ReplaySender.java b/src/main/java/eu/crushedpixel/replaymod/replay/ReplaySender.java index 85de915e..fbb6420a 100755 --- a/src/main/java/eu/crushedpixel/replaymod/replay/ReplaySender.java +++ b/src/main/java/eu/crushedpixel/replaymod/replay/ReplaySender.java @@ -1,22 +1,19 @@ package eu.crushedpixel.replaymod.replay; -import com.google.gson.Gson; +import com.google.common.base.Preconditions; import eu.crushedpixel.replaymod.ReplayMod; import eu.crushedpixel.replaymod.entities.CameraEntity; import eu.crushedpixel.replaymod.events.RecordingHandler; -import eu.crushedpixel.replaymod.holders.KeyframeSet; import eu.crushedpixel.replaymod.holders.PacketData; import eu.crushedpixel.replaymod.holders.Position; -import eu.crushedpixel.replaymod.recording.ConnectionEventHandler; -import eu.crushedpixel.replaymod.recording.ReplayMetaData; import eu.crushedpixel.replaymod.timer.MCTimerHandler; +import eu.crushedpixel.replaymod.utils.ReplayFile; import eu.crushedpixel.replaymod.utils.ReplayFileIO; import io.netty.channel.ChannelHandler.Sharable; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import net.minecraft.client.Minecraft; import net.minecraft.client.resources.ResourcePackRepository; -import net.minecraft.entity.Entity; import net.minecraft.network.EnumConnectionState; import net.minecraft.network.NetworkManager; import net.minecraft.network.Packet; @@ -24,208 +21,177 @@ import net.minecraft.network.play.server.*; import net.minecraft.world.EnumDifficulty; import net.minecraft.world.WorldSettings.GameType; import net.minecraft.world.WorldType; -import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; -import org.apache.commons.compress.archivers.zip.ZipFile; import org.apache.commons.io.FileUtils; -import org.apache.commons.io.FilenameUtils; -import java.io.*; +import java.io.DataInputStream; +import java.io.EOFException; +import java.io.File; +import java.io.IOException; import java.net.URL; -import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +/** + * Sends replay packets to netty channels. + * Even though {@link Sharable}, this should never be added to multiple pipes at once, it may however be re-added when + * the replay restart from the beginning. + */ @Sharable public class ReplaySender extends ChannelInboundHandlerAdapter { - private int currentTimeStamp; - private boolean hurryToTimestamp; - private long desiredTimeStamp = -1; - private long toleratedTimeStamp = -1; - private long lastTimeStamp, lastPacketSent; - private boolean hasRestarted = false; - private File replayFile; - private boolean active = true; - private ZipFile archive; - private DataInputStream dis; - private ChannelHandlerContext ctx = null; - private boolean startFromBeginning = true; - private NetworkManager networkManager; - private boolean terminate = false; - private double replaySpeed = 1f; - private boolean hasWorldLoaded = false; - private Minecraft mc = Minecraft.getMinecraft(); - private int replayLength = 0; - private int actualID = -1; - private ZipArchiveEntry replayEntry; - private ArrayList badPackets = new ArrayList() { - { - add(S28PacketEffect.class); - add(S2BPacketChangeGameState.class); - add(S06PacketUpdateHealth.class); - add(S2DPacketOpenWindow.class); - add(S2EPacketCloseWindow.class); - add(S2FPacketSetSlot.class); - add(S30PacketWindowItems.class); - add(S36PacketSignEditorOpen.class); - add(S37PacketStatistics.class); - add(S1FPacketSetExperience.class); - add(S43PacketCamera.class); - add(S39PacketPlayerAbilities.class); - } - }; - private boolean allowMovement = false; - private Thread sender = new Thread(new Runnable() { + /** + * These packets are ignored completely during replay. + */ + private static final List BAD_PACKETS = Arrays.asList( + S28PacketEffect.class, + S2BPacketChangeGameState.class, + S06PacketUpdateHealth.class, + S2DPacketOpenWindow.class, + S2EPacketCloseWindow.class, + S2FPacketSetSlot.class, + S30PacketWindowItems.class, + S36PacketSignEditorOpen.class, + S37PacketStatistics.class, + S1FPacketSetExperience.class, + S43PacketCamera.class, + S39PacketPlayerAbilities.class); - @Override - public void run() { - try { - dis = new DataInputStream(archive.getInputStream(replayEntry)); - } catch(Exception e) { - e.printStackTrace(); - } + /** + * Whether to work in async mode. + * + * When in async mode, a separate thread send packets and waits according to their delays. + * This is default in normal playback mode. + * + * When in sync mode, no packets will be sent until {@link #sendPacketsTill(int)} is called. + * This is used during path playback and video rendering. + */ + protected boolean asyncMode; - try { - while(ctx == null && !terminate) { - Thread.sleep(10); - } - while(!terminate) { - if(startFromBeginning) { - hasRestarted = true; - hasWorldLoaded = false; - currentTimeStamp = 0; - dis.close(); - dis = new DataInputStream(archive.getInputStream(replayEntry)); - startFromBeginning = false; - lastPacketSent = System.currentTimeMillis(); - ReplayHandler.restartReplay(); - } + /** + * Timestamp of the last packet sent in milliseconds since the start. + */ + protected int lastTimeStamp; - while(!terminate && !startFromBeginning && (!paused() || !hasWorldLoaded)) { - try { - /* - * LOGIC: - * While behind desired timestamp, only send packets - * until desired timestamp is reached, - * then increase desired timestamp by 1/20th of a second - * - * Desired timestamp is divided through stretch factor. - * - * If hurrying, don't wait for correct timing. - */ + /** + * Whether the replay has been restarted. + * It might be required to advance some ticks in order for rendering to keep up. + */ + protected boolean hasRestarted; - if(!hurryToTimestamp && ReplayHandler.isInPath()) { - continue; - } + /** + * The replay file. + */ + protected ReplayFile replayFile; - PacketData pd = ReplayFileIO.readPacketData(dis); + /** + * The channel handler context used to send packets to minecraft. + */ + protected ChannelHandlerContext ctx; - currentTimeStamp = pd.getTimestamp(); + /** + * The data input stream from which new packets are read. + * When accessing this stream make sure to synchronize on {@code this} as it's used from multiple threads. + */ + protected DataInputStream dis; - if(!ReplayHandler.isInPath() && !hurryToTimestamp && hasWorldLoaded) { - int timeWait = (int) Math.round((currentTimeStamp - lastTimeStamp) / replaySpeed); - long timeDiff = System.currentTimeMillis() - lastPacketSent; - lastPacketSent = System.currentTimeMillis(); - long timeToSleep = Math.max(0, timeWait - timeDiff); - Thread.sleep(timeToSleep); - } + /** + * The next packet that should be sent. + * This is required as some actions such as jumping to a specified timestamp have to peek at the next packet. + */ + protected PacketData nextPacket; - ReplaySender.this.channelRead(ctx, pd.getByteArray()); + /** + * Whether we need to restart the current replay. E.g. when jumping backwards in time + */ + protected boolean startFromBeginning = true; - lastTimeStamp = currentTimeStamp; + /** + * Whether to terminate the replay. This only has an effect on the async mode and is {@code true} during sync mode. + */ + protected boolean terminate; - if(hurryToTimestamp && currentTimeStamp >= desiredTimeStamp && !startFromBeginning) { - hurryToTimestamp = false; - if(!ReplayHandler.isInPath() || hasRestarted) { - MCTimerHandler.advanceRenderPartialTicks(5); - MCTimerHandler.advancePartialTicks(5); - MCTimerHandler.advanceTicks(5); - } - if(!ReplayHandler.isInPath()) { - Position pos = ReplayHandler.getLastPosition(); - CameraEntity cam = ReplayHandler.getCameraEntity(); - if(cam != null) { - if(Math.abs(pos.getX() - cam.posX) < ReplayMod.TP_DISTANCE_LIMIT && Math.abs(pos.getZ() - cam.posZ) < ReplayMod.TP_DISTANCE_LIMIT) - if(pos != null) { - cam.moveAbsolute(pos.getX(), pos.getY(), pos.getZ()); - cam.rotationPitch = pos.getPitch(); - cam.rotationYaw = pos.getYaw(); - } - } - } - if(!ReplayHandler.isInPath()) { - setReplaySpeed(0); - } - hasRestarted = false; - } + /** + * The speed of the replay. 1 is normal, 2 is twice as fast, 0.5 is half speed and 0 is frozen + */ + protected double replaySpeed = 1f; - } catch(EOFException eof) { - dis = new DataInputStream(archive.getInputStream(replayEntry)); - setReplaySpeed(0); - } catch(IOException e) { - e.printStackTrace(); - } - } - } - } catch(Exception e) { - e.printStackTrace(); - } - } - }); + /** + * Whether the world has been loaded and the dirt-screen should go away. + */ + protected boolean hasWorldLoaded; - public ReplaySender(final File replayFile, NetworkManager nm) { - this.replayFile = replayFile; - this.networkManager = nm; - if(("." + FilenameUtils.getExtension(replayFile.getAbsolutePath())).equals(ConnectionEventHandler.ZIP_FILE_EXTENSION)) { - try { - archive = new ZipFile(replayFile); - replayEntry = archive.getEntry("recording" + ConnectionEventHandler.TEMP_FILE_EXTENSION); + /** + * The minecraft instance. + */ + protected Minecraft mc = Minecraft.getMinecraft(); - ZipArchiveEntry metadata = archive.getEntry("metaData" + ConnectionEventHandler.JSON_FILE_EXTENSION); - InputStream is = archive.getInputStream(metadata); - BufferedReader br = new BufferedReader(new InputStreamReader(is)); + /** + * The total length of this replay in milliseconds. + */ + protected final int replayLength; - String json = br.readLine(); + /** + * Our actual entity id that the server gave to us. + */ + protected int actualID = -1; - Gson gson = new Gson(); - ReplayMetaData metaData = gson.fromJson(json, ReplayMetaData.class); + /** + * Whether to allow (process) the next player movement packet. + */ + protected boolean allowMovement; - this.replayLength = metaData.getDuration(); + /** + * Create a new replay sender. + * @param file The replay file + * @param asyncMode {@code true} for async mode, {@code false} otherwise + * @see #asyncMode + */ + public ReplaySender(ReplayFile file, boolean asyncMode) { + this.replayFile = file; + this.asyncMode = asyncMode; + this.replayLength = file.metadata().get().getDuration(); - ZipArchiveEntry paths = archive.getEntry("paths"); - if(paths != null) { - InputStream is2 = archive.getInputStream(paths); - BufferedReader br2 = new BufferedReader(new InputStreamReader(is2)); - - String json2 = br2.readLine(); - KeyframeSet[] repo = gson.fromJson(json2, KeyframeSet[].class); - - ReplayHandler.setKeyframeRepository(repo, false); - } else { - ReplayHandler.setKeyframeRepository(new KeyframeSet[]{}, false); - } - - sender.start(); - } catch(Exception e) { - e.printStackTrace(); - } + if (asyncMode) { + new Thread(asyncSender).start(); } } - public boolean isHurrying() { - return hurryToTimestamp; + /** + * Set whether this replay sender operates in async mode. + * When in async mode, it will send packets timed from a separate thread. + * When not in async mode, it will send packets when {@link #sendPacketsTill(int)} is called. + * @param asyncMode {@code true} to enable async mode + */ + public void setAsyncMode(boolean asyncMode) { + if (this.asyncMode == asyncMode) return; + this.asyncMode = asyncMode; + if (asyncMode) { + this.terminate = false; + new Thread(asyncSender).start(); + } else { + this.terminate = true; + } } + /** + * Return the timestamp of the last packet sent. + * @return The timestamp in milliseconds since the start of the replay + */ public int currentTimeStamp() { - return currentTimeStamp; + return lastTimeStamp; } + /** + * Return the total length of the replay played. + * @return Total length in milliseconds + */ public int replayLength() { return replayLength; } - public void stopHurrying() { - hurryToTimestamp = false; - } - + /** + * Terminate this replay sender. + */ public void terminateReplay() { terminate = true; try { @@ -236,264 +202,174 @@ public class ReplaySender extends ChannelInboundHandlerAdapter { } } - public long getDesiredTimestamp() { - return desiredTimeStamp; - } - - public void resetToleratedTimeStamp() { - toleratedTimeStamp = -1; - } - - public void jumpToTime(int millis) { - if(!(ReplayHandler.isInPath() && ReplayProcess.isVideoRecording())) setReplaySpeed(replaySpeed); - - if((millis < currentTimeStamp && !isHurrying())) { - if(ReplayHandler.isInPath()) { - if(millis >= toleratedTimeStamp && toleratedTimeStamp >= 0) { - return; - } - } - startFromBeginning = true; - } - - desiredTimeStamp = millis; - if(ReplayHandler.isInPath()) { - toleratedTimeStamp = millis; - } - hurryToTimestamp = true; - - } - - //private static Field dataWatcherField; - @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { - if(terminate) { + // When in async mode and the replay sender shut down, then don't send packets + if(terminate && asyncMode) { return; } - if(ctx == null) { - ctx = this.ctx; - } - + // When a packet is sent directly, perform no filtering if(msg instanceof Packet) { super.channelRead(ctx, msg); - return; } - byte[] ba = (byte[]) msg; - - try { - Packet p = ReplayFileIO.deserializePacket(ba); - - if(p == null) return; - - //If hurrying, ignore some packets, unless during Replay Path and *not* in initial hurry - if(hurryToTimestamp && (!ReplayHandler.isInPath() || (desiredTimeStamp - currentTimeStamp > 1000))) { - if(p instanceof S45PacketTitle || - p instanceof S2APacketParticles) return; - - if(p instanceof S0EPacketSpawnObject) { - S0EPacketSpawnObject pso = (S0EPacketSpawnObject)p; - int type = pso.func_148993_l(); - if(type == 76) { - return; - } - } - } - - if(p instanceof S29PacketSoundEffect && ReplayHandler.isInPath() && ReplayProcess.isVideoRecording()) { - return; - } - - if(p instanceof S03PacketTimeUpdate) { - p = TimeHandler.getTimePacket((S03PacketTimeUpdate) p); - } - - if(p instanceof S48PacketResourcePackSend) { - S48PacketResourcePackSend pa = (S48PacketResourcePackSend) p; - Thread t = new ResourcePackCheck(pa.func_179783_a(), pa.func_179784_b()); - t.start(); - - return; - } - - if(badPackets.contains(p.getClass())) return; - - /* - if(p instanceof S0EPacketSpawnObject) { - if(mc.theWorld != null) { - List arrows = mc.theWorld.getEntities(EntityArrow.class, new Predicate() { - @Override - public boolean apply(EntityArrow input) { - return true; - } - }); - if(arrows.size() > 20) { - System.out.println(currentTimeStamp); - } - } - } - */ + if (msg instanceof byte[]) { try { - if(p instanceof S1CPacketEntityMetadata) { - S1CPacketEntityMetadata packet = (S1CPacketEntityMetadata) p; - if(packet.field_149379_a == actualID) { - packet.field_149379_a = RecordingHandler.entityID; + Packet p = ReplayFileIO.deserializePacket((byte[]) msg); + + if (p != null) { + p = processPacket(p); + if (p != null) { + super.channelRead(ctx, p); } } - - if(p instanceof S01PacketJoinGame) { - S01PacketJoinGame packet = (S01PacketJoinGame) p; - allowMovement = true; - int entId = packet.getEntityId(); - actualID = entId; - entId = Integer.MIN_VALUE + 9002; - int dimension = packet.getDimension(); - EnumDifficulty difficulty = packet.getDifficulty(); - int maxPlayers = packet.getMaxPlayers(); - WorldType worldType = packet.getWorldType(); - - p = new S01PacketJoinGame(entId, GameType.SPECTATOR, false, dimension, - difficulty, maxPlayers, worldType, false); - } - - if(p instanceof S07PacketRespawn) { - S07PacketRespawn respawn = (S07PacketRespawn) p; - p = new S07PacketRespawn(respawn.func_149082_c(), - respawn.func_149081_d(), respawn.func_149080_f(), GameType.SPECTATOR); - - allowMovement = true; - } - - /* - * Proof of concept for some nasty player manipulation ;) - String crPxl = "2cb08a5951f34e98bd0985d9747e80df"; - String johni = "cd3d4be14ffc2f9db432db09e0cd254b"; - - if(p instanceof S38PacketPlayerListItem) { - S38PacketPlayerListItem pp = (S38PacketPlayerListItem)p; - if(((AddPlayerData)pp.func_179767_a().get(0)).func_179962_a().getId().toString().replace("-", "").equals(crPxl)) { - GameProfile johniGP = new GameProfile(UUID.fromString(johni.replaceAll( - "(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})", - "$1-$2-$3-$4-$5")), "Johni0702"); - gameProfileField.set(pp.func_179767_a().get(0), johniGP); - //pp.func_179767_a().set(0, johniGP); - p = pp; - } - - } - - if(p instanceof S0CPacketSpawnPlayer) { - S0CPacketSpawnPlayer sp = (S0CPacketSpawnPlayer)p; - - if(sp.func_179819_c().toString().replace("-", "").equals(crPxl)) { - playerUUIDField.set(sp, UUID.fromString(johni.replaceAll( - "(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})", - "$1-$2-$3-$4-$5"))); - } - - p = sp; - } - */ - - /* - if(p instanceof S0CPacketSpawnPlayer) { - System.out.println(dataWatcherField.get(p)); - System.out.println(((S0CPacketSpawnPlayer) p).func_148944_c()); - } - */ - - if(p instanceof S08PacketPlayerPosLook) { - if(!hasWorldLoaded) hasWorldLoaded = true; - final S08PacketPlayerPosLook ppl = (S08PacketPlayerPosLook) p; - - if(ReplayHandler.isInPath() && !hurryToTimestamp) return; - - CameraEntity cent = ReplayHandler.getCameraEntity(); - - if(cent != null) { - if(!allowMovement && !((Math.abs(cent.posX - ppl.func_148932_c()) > ReplayMod.TP_DISTANCE_LIMIT) || - (Math.abs(cent.posZ - ppl.func_148933_e()) > ReplayMod.TP_DISTANCE_LIMIT))) { - return; - } else { - allowMovement = false; - } - } - - Thread t = new Thread(new Runnable() { - - @Override - public void run() { - while(mc.theWorld == null) { - try { - Thread.sleep(10); - } catch(InterruptedException e) { - e.printStackTrace(); - } - } - - Entity ent = ReplayHandler.getCameraEntity(); - - if(ent == null || !(ent instanceof CameraEntity)) ent = new CameraEntity(mc.theWorld); - CameraEntity cent = (CameraEntity) ent; - cent.moveAbsolute(ppl.func_148932_c(), ppl.func_148928_d(), ppl.func_148933_e()); - - ReplayHandler.setCameraEntity(cent); - } - }); - - t.start(); - } - - if(p instanceof S43PacketCamera) { - return; - } - - super.channelRead(ctx, p); - } catch(Exception e) { - System.out.println(p.getClass()); + } catch (Exception e) { + // We'd rather not have a failure parsing one packet screw up the whole replay process e.printStackTrace(); } - - } catch(Exception e) { - e.printStackTrace(); } } + /** + * Process a packet and return the result. + * @param p The packet to process + * @return The processed packet or {@code null} if no packet shall be sent + */ + protected Packet processPacket(Packet p) { + if(BAD_PACKETS.contains(p.getClass())) return null; + + if(p instanceof S29PacketSoundEffect && ReplayProcess.isVideoRecording()) { + return null; + } + + if(p instanceof S03PacketTimeUpdate) { + p = TimeHandler.getTimePacket((S03PacketTimeUpdate) p); + } + + if(p instanceof S48PacketResourcePackSend) { + S48PacketResourcePackSend packet = (S48PacketResourcePackSend) p; + new ResourcePackCheck(packet.func_179783_a(), packet.func_179784_b()).start(); + return null; + } + + if(p instanceof S1CPacketEntityMetadata) { + S1CPacketEntityMetadata packet = (S1CPacketEntityMetadata) p; + if(packet.field_149379_a == actualID) { + packet.field_149379_a = RecordingHandler.entityID; + } + } + + if(p instanceof S01PacketJoinGame) { + S01PacketJoinGame packet = (S01PacketJoinGame) p; + allowMovement = true; + int entId = packet.getEntityId(); + actualID = entId; + entId = Integer.MIN_VALUE + 9002; + int dimension = packet.getDimension(); + EnumDifficulty difficulty = packet.getDifficulty(); + int maxPlayers = packet.getMaxPlayers(); + WorldType worldType = packet.getWorldType(); + + p = new S01PacketJoinGame(entId, GameType.SPECTATOR, false, dimension, + difficulty, maxPlayers, worldType, false); + } + + if(p instanceof S07PacketRespawn) { + S07PacketRespawn respawn = (S07PacketRespawn) p; + p = new S07PacketRespawn(respawn.func_149082_c(), + respawn.func_149081_d(), respawn.func_149080_f(), GameType.SPECTATOR); + + allowMovement = true; + } + + if(p instanceof S08PacketPlayerPosLook) { + if(!hasWorldLoaded) hasWorldLoaded = true; + final S08PacketPlayerPosLook ppl = (S08PacketPlayerPosLook) p; + + if(ReplayHandler.isInPath()) return null; + + CameraEntity cent = ReplayHandler.getCameraEntity(); + + if(cent != null) { + if(!allowMovement && !((Math.abs(cent.posX - ppl.func_148932_c()) > ReplayMod.TP_DISTANCE_LIMIT) || + (Math.abs(cent.posZ - ppl.func_148933_e()) > ReplayMod.TP_DISTANCE_LIMIT))) { + return null; + } else { + allowMovement = false; + } + } + + mc.addScheduledTask(new Runnable() { + + @Override + public void run() { + if (mc.theWorld == null) { + mc.addScheduledTask(this); + return; + } + + CameraEntity cent = ReplayHandler.getCameraEntity(); + + if (cent == null){ + cent = new CameraEntity(mc.theWorld); + } + cent.moveAbsolute(ppl.func_148932_c(), ppl.func_148928_d(), ppl.func_148933_e()); + ReplayHandler.setCameraEntity(cent); + } + }); + } + + return asyncMode ? processPacketAsync(p) : processPacketSync(p); + } + @Override + @SuppressWarnings("unchecked") public void channelActive(ChannelHandlerContext ctx) throws Exception { this.ctx = ctx; - networkManager.channel().attr(networkManager.attrKeyConnectionState).set(EnumConnectionState.PLAY); + ctx.attr(NetworkManager.attrKeyConnectionState).set(EnumConnectionState.PLAY); super.channelActive(ctx); } @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { - archive.close(); + replayFile.close(); super.channelInactive(ctx); } + /** + * Whether the replay is currently paused. + * @return {@code true} if it is paused, {@code false} otherwise + */ public boolean paused() { return MCTimerHandler.getTimerSpeed() == 0; } + /** + * Returns the speed of the replay. 1 being normal speed, 0.5 half and 2 twice as fast. + * If 0 is returned, the replay is paused. + * @return speed multiplier + */ public double getReplaySpeed() { if(!paused()) return replaySpeed; else return 0; } + /** + * Set the speed of the replay. 1 being normal speed, 0.5 half and 2 twice as fast. + * The speed may not be set to 0 nor to negative values. + * @param d Speed multiplier + */ public void setReplaySpeed(final double d) { if(d != 0) this.replaySpeed = d; MCTimerHandler.setTimerSpeed((float) d); } - public File getReplayFile() { - return replayFile; - } - + /** + * Checks for stored resource packs and loads them or downloads a new one. + */ private static class ResourcePackCheck extends Thread { private static Minecraft mc = Minecraft.getMinecraft(); @@ -506,7 +382,13 @@ public class ReplaySender extends ChannelInboundHandlerAdapter { this.hash = hash; } - private File getServerResourcePackLocation(String url, String hash) throws IOException, IllegalArgumentException, IllegalAccessException { + /** + * Return the location of the stored resource pack. + * @param url The original url of the resource pack + * @param hash The hash code of the resource pack + * @return File location of the resource pack + */ + private File getServerResourcePackLocation(String url, String hash) { String filename; @@ -529,6 +411,12 @@ public class ReplaySender extends ChannelInboundHandlerAdapter { return new File(repo.dirServerResourcepacks, filename); } + /** + * Download a resource pack from a specified URL. + * @param url The URL to download from + * @param file The target file location + * @return {@code true} if the download was successful, {@code false} if an I/O-error occured + */ private boolean downloadServerResourcePack(String url, File file) { try { FileUtils.copyURLToFile(new URL(url), file); @@ -539,10 +427,14 @@ public class ReplaySender extends ChannelInboundHandlerAdapter { return false; } + /** + * Add the resource pack to the loaded resource packs if resource packs are enabled in the config. + * If there is no local copy of the resource pack, this loads the resource pack from the specified url. + */ @Override public void run() { try { - boolean use = ReplayMod.instance.replaySettings.getUseResourcePacks(); + boolean use = ReplayMod.replaySettings.getUseResourcePacks(); if(!use) return; System.out.println("Looking for downloaded Resource Pack..."); @@ -571,4 +463,286 @@ public class ReplaySender extends ChannelInboundHandlerAdapter { } } + ///////////////////////////////////////////////////////// + // Asynchronous packet processing // + ///////////////////////////////////////////////////////// + + /** + * The real time at which the last packet was sent in milliseconds. + */ + private long lastPacketSent; + + /** + * There is no waiting performed until a packet with at least this timestamp is reached (but not yet sent). + * If this is -1, then timing is normal. + */ + private long desiredTimeStamp = -1; + + /** + * Runnable which performs timed dispatching of packets from the input stream. + */ + private Runnable asyncSender = new Runnable() { + public void run() { + try { + while (ctx == null && !terminate) { + Thread.sleep(10); + } + REPLAY_LOOP: + while (true) { + synchronized (ReplaySender.this) { + if (dis == null) { + dis = new DataInputStream(replayFile.recording().get()); + } + // Packet loop + while (true) { + try { + // When playback is paused and the world has loaded (we don't want any dirt-screens) we sleep + while (paused() && hasWorldLoaded) { + // Unless we are going to terminate, restart or jump + if (terminate || startFromBeginning || desiredTimeStamp != -1) { + break; + } + Thread.sleep(10); + } + + if (terminate) { + break REPLAY_LOOP; + } + + if (startFromBeginning) { + // In case we need to restart from the beginning + // break out of the loop sending all packets which will + // cause the replay to be restarted by the outer loop + break; + } + + // Read the next packet if we don't already have one + if (nextPacket == null) { + nextPacket = ReplayFileIO.readPacketData(dis); + } + + int nextTimeStamp = nextPacket.getTimestamp(); + + // If we aren't jumping and the world has already been loaded (no dirt-screens) then wait + // the required amount to get proper packet timing + if (!isHurrying() && hasWorldLoaded) { + // How much time should have passed + int timeWait = (int) Math.round((nextTimeStamp - lastTimeStamp) / replaySpeed); + // How much time did pass + long timeDiff = System.currentTimeMillis() - lastPacketSent; + // How much time we need to wait to make up for the difference + long timeToSleep = Math.max(0, timeWait - timeDiff); + + Thread.sleep(timeToSleep); + lastPacketSent = System.currentTimeMillis(); + } + + // Process packet + channelRead(ctx, nextPacket.getByteArray()); + nextPacket = null; + + lastTimeStamp = nextTimeStamp; + + // In case we finished jumping + // We need to check that we aren't planing to restart so we don't accidentally run this + // code before we actually restarted + if (isHurrying() && lastTimeStamp > desiredTimeStamp && !startFromBeginning) { + desiredTimeStamp = -1; + + // Give the render engine a reason to get going + MCTimerHandler.advanceRenderPartialTicks(5); + MCTimerHandler.advancePartialTicks(5); + MCTimerHandler.advanceTicks(5); + + Position pos = ReplayHandler.getLastPosition(); + CameraEntity cam = ReplayHandler.getCameraEntity(); + if (cam != null && pos != null) { + // Move camera back in case we have been respawned + if (Math.abs(pos.getX() - cam.posX) < ReplayMod.TP_DISTANCE_LIMIT && Math.abs(pos.getZ() - cam.posZ) < ReplayMod.TP_DISTANCE_LIMIT) { + cam.moveAbsolute(pos.getX(), pos.getY(), pos.getZ()); + cam.rotationPitch = pos.getPitch(); + cam.rotationYaw = pos.getYaw(); + } + } + + // Pause after jumping + setReplaySpeed(0); + } + } catch (EOFException eof) { + // Reached end of file + // Pause the replay which will cause it to freeze before getting restarted + setReplaySpeed(0); + // Then wait until the user tells us to continue + while (paused() && hasWorldLoaded && desiredTimeStamp == -1) { + Thread.sleep(10); + } + break; + } catch (IOException e) { + e.printStackTrace(); + } + } + + // Restart the replay. + hasRestarted = true; + hasWorldLoaded = false; + lastTimeStamp = 0; + startFromBeginning = false; + nextPacket = null; + lastPacketSent = System.currentTimeMillis(); + ReplayHandler.restartReplay(); + if (dis != null) { + dis.close(); + dis = null; + } + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + }; + + /** + * Return whether this replay sender is currently rushing. When rushing, all packets are sent without waiting until + * a specified timestamp is passed. + * @return {@code true} if currently rushing, {@code false} otherwise + */ + public boolean isHurrying() { + return desiredTimeStamp != -1; + } + + /** + * Cancels the hurrying. + */ + public void stopHurrying() { + desiredTimeStamp = -1; + } + + /** + * Return the timestamp to which this replay sender is currently rushing. All packets with an lower or equal + * timestamp will be sent out without any sleeping. + * @return The timestamp in milliseconds since the start of the replay + */ + public long getDesiredTimestamp() { + return desiredTimeStamp; + } + + /** + * Jumps to the specified timestamp when in async mode by rushing all packets until one with a timestamp greater + * than the specified timestamp is found. + * If the timestamp has already passed, this causes the replay to restart and then rush all packets. + * @param millis Timestamp in milliseconds since the start of the replay + */ + public void jumpToTime(int millis) { + Preconditions.checkState(asyncMode, "Can only jump in async mode. Use sendPacketsTill(int) instead."); + if(millis < lastTimeStamp && !isHurrying()) { + startFromBeginning = true; + } + + desiredTimeStamp = millis; + } + + protected Packet processPacketAsync(Packet p) { + //If hurrying, ignore some packets, unless during Replay Path and *not* in short hurries + if(!ReplayHandler.isInPath() && desiredTimeStamp - lastTimeStamp > 1000) { + if(p instanceof S2APacketParticles) return null; + + if(p instanceof S0EPacketSpawnObject) { + S0EPacketSpawnObject pso = (S0EPacketSpawnObject)p; + int type = pso.func_148993_l(); + if(type == 76) { // Firework rocket + return null; + } + } + } + return p; + } + + ///////////////////////////////////////////////////////// + // Synchronous packet processing // + ///////////////////////////////////////////////////////// + + /** + * Sends all packets until the specified timestamp is reached (inclusive). + * If the timestamp is smaller than the last packet sent, the replay is restarted from the beginning. + * @param timestamp The timestamp in milliseconds since the beginning of this replay + */ + public void sendPacketsTill(int timestamp) { + Preconditions.checkState(!asyncMode, "This method cannot be used in async mode. Use jumpToTime(int) instead."); + try { + while (ctx == null && !terminate) { // Make sure channel is ready + Thread.sleep(10); + } + + synchronized (this) { + if (timestamp < lastTimeStamp) { // Restart the replay if we need to go backwards in time + hasRestarted = true; + hasWorldLoaded = false; + lastTimeStamp = 0; + if (dis != null) { + dis.close(); + dis = null; + } + startFromBeginning = false; + nextPacket = null; + ReplayHandler.restartReplay(); + } + + if (dis == null) { + dis = new DataInputStream(replayFile.recording().get()); + } + + while (true) { // Send packets + try { + PacketData pd; + if (nextPacket != null) { + // If there is still a packet left from before, use it first + pd = nextPacket; + nextPacket = null; + } else { + // Otherwise read one from the input stream + pd = ReplayFileIO.readPacketData(dis); + } + + int nextTimeStamp = pd.getTimestamp(); + if (nextTimeStamp > timestamp) { + // We are done sending all packets + nextPacket = pd; + break; + } + + // Process packet + channelRead(ctx, pd.getByteArray()); + + // Store last timestamp + lastTimeStamp = nextTimeStamp; + } catch (EOFException eof) { + // Shit! We hit the end before finishing our job! What shall we do now? + // well, let's just pretend we're done... + dis = null; + break; + } catch (IOException e) { + e.printStackTrace(); + } + } + + // This might be required if we change to async mode anytime soon + lastPacketSent = System.currentTimeMillis(); + + // In case we have restarted the replay we have to give the render engine a reason to get going + if (hasRestarted) { + MCTimerHandler.advanceRenderPartialTicks(5); + MCTimerHandler.advancePartialTicks(5); + MCTimerHandler.advanceTicks(5); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + protected Packet processPacketSync(Packet p) { + return p; // During synchronous playback everything is sent normally + } + } diff --git a/src/main/java/eu/crushedpixel/replaymod/utils/ReplayFile.java b/src/main/java/eu/crushedpixel/replaymod/utils/ReplayFile.java new file mode 100644 index 00000000..d6bceeee --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/utils/ReplayFile.java @@ -0,0 +1,117 @@ +package eu.crushedpixel.replaymod.utils; + +import com.google.common.base.Supplier; +import com.google.gson.Gson; +import eu.crushedpixel.replaymod.holders.KeyframeSet; +import eu.crushedpixel.replaymod.recording.ReplayMetaData; +import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; +import org.apache.commons.compress.archivers.zip.ZipFile; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.*; + +public class ReplayFile extends ZipFile { + + public static final String TEMP_FILE_EXTENSION = ".tmcpr"; + public static final String JSON_FILE_EXTENSION = ".json"; + public static final String ZIP_FILE_EXTENSION = ".mcpr"; + public static final String ENTRY_RECORDING = "recording" + TEMP_FILE_EXTENSION; + public static final String ENTRY_METADATA = "metaData" + JSON_FILE_EXTENSION; + public static final String ENTRY_PATHS = "paths"; + public static final String ENTRY_THUMB = "thumb"; + + private final File file; + + public ReplayFile(File f) throws IOException { + super(f); + this.file = f; + } + + public File getFile() { + return file; + } + + public ZipArchiveEntry recordingEntry() { + return getEntry(ENTRY_RECORDING); + } + + public Supplier recording() { + return new Supplier() { + @Override + public InputStream get() { + try { + return getInputStream(recordingEntry()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + }; + } + + public ZipArchiveEntry metadataEntry() { + return getEntry(ENTRY_METADATA); + } + + public Supplier metadata() { + return new Supplier() { + @Override + public ReplayMetaData get() { + try { + BufferedReader reader = new BufferedReader(new InputStreamReader(getInputStream(metadataEntry()))); + return new Gson().fromJson(reader, ReplayMetaData.class); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + }; + } + + public ZipArchiveEntry pathsEntry() { + return getEntry(ENTRY_PATHS); + } + + public Supplier paths() { + return new Supplier() { + @Override + public KeyframeSet[] get() { + try { + ZipArchiveEntry entry = pathsEntry(); + if (entry == null) { + return null; + } + BufferedReader reader = new BufferedReader(new InputStreamReader(getInputStream(entry))); + return new Gson().fromJson(reader, KeyframeSet[].class); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + }; + } + + public ZipArchiveEntry thumbEntry() { + return getEntry(ENTRY_THUMB); + } + + public Supplier thumb() { + return new Supplier() { + @Override + public BufferedImage get() { + try { + ZipArchiveEntry entry = thumbEntry(); + if (entry == null) { + return null; + } + InputStream is = getInputStream(entry); + int i = 7; + while (i > 0) { + i -= is.skip(i); // Security through obscurity \o/ + } + return ImageIO.read(is); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + }; + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/utils/ReplayFileIO.java b/src/main/java/eu/crushedpixel/replaymod/utils/ReplayFileIO.java index 3e4c3065..1405818d 100755 --- a/src/main/java/eu/crushedpixel/replaymod/utils/ReplayFileIO.java +++ b/src/main/java/eu/crushedpixel/replaymod/utils/ReplayFileIO.java @@ -1,11 +1,9 @@ package eu.crushedpixel.replaymod.utils; -import akka.japi.Pair; import com.google.gson.Gson; import eu.crushedpixel.replaymod.ReplayMod; import eu.crushedpixel.replaymod.holders.KeyframeSet; import eu.crushedpixel.replaymod.holders.PacketData; -import eu.crushedpixel.replaymod.recording.ConnectionEventHandler; import eu.crushedpixel.replaymod.recording.PacketSerializer; import eu.crushedpixel.replaymod.recording.ReplayMetaData; import io.netty.buffer.ByteBuf; @@ -16,8 +14,6 @@ import net.minecraft.network.Packet; import net.minecraft.network.PacketBuffer; import net.minecraft.network.play.server.S01PacketJoinGame; import net.minecraft.network.play.server.S08PacketPlayerPosLook; -import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; -import org.apache.commons.compress.archivers.zip.ZipFile; import org.apache.commons.io.FilenameUtils; import java.io.*; @@ -54,28 +50,13 @@ public class ReplayFileIO { List files = new ArrayList(); File folder = getReplayFolder(); for(File file : folder.listFiles()) { - if(("." + FilenameUtils.getExtension(file.getAbsolutePath())).equals( - ConnectionEventHandler.ZIP_FILE_EXTENSION)) { + if(("." + FilenameUtils.getExtension(file.getAbsolutePath())).equals(ReplayFile.ZIP_FILE_EXTENSION)) { files.add(file); } } return files; } - private static DataInputStream getMetaDataInputStream(File replayFile) throws IOException { - ZipFile archive = null; - - try { - archive = new ZipFile(replayFile); - ZipArchiveEntry tmcpr = archive.getEntry("metaData" + - ConnectionEventHandler.JSON_FILE_EXTENSION); - - return new DataInputStream(archive.getInputStream(tmcpr)); - } catch(IOException e) { - throw e; - } - } - public static void writeReplayFile(File replayFile, File tempFile, ReplayMetaData metaData) throws IOException { byte[] buffer = new byte[1024]; @@ -88,13 +69,13 @@ public class ReplayFileIO { String json = new Gson().toJson(metaData); - zos.putNextEntry(new ZipEntry("metaData.json")); + zos.putNextEntry(new ZipEntry(ReplayFile.ENTRY_METADATA)); PrintWriter pw = new PrintWriter(zos); pw.write(json); pw.flush(); zos.closeEntry(); - zos.putNextEntry(new ZipEntry("recording" + ConnectionEventHandler.TEMP_FILE_EXTENSION)); + zos.putNextEntry(new ZipEntry(ReplayFile.ENTRY_RECORDING)); FileInputStream fis = new FileInputStream(tempFile); int len; while((len = fis.read(buffer)) > 0) { @@ -107,26 +88,13 @@ public class ReplayFileIO { zos.close(); } - private static Pair getTempFileInputStream(File replayFile) throws Exception { - ZipFile archive = null; - - try { - archive = new ZipFile(replayFile); - ZipArchiveEntry tmcpr = archive.getEntry("recording" + - ConnectionEventHandler.TEMP_FILE_EXTENSION); - long size = tmcpr.getSize(); - - return new Pair(size, new DataInputStream(archive.getInputStream(tmcpr))); - } catch(Exception e) { - throw e; - } - } - public static ReplayMetaData getMetaData(File replayFile) throws IOException { - BufferedReader br = new BufferedReader(new InputStreamReader(getMetaDataInputStream(replayFile))); - String json = br.readLine(); - - return new Gson().fromJson(json, ReplayMetaData.class); + ReplayFile file = new ReplayFile(replayFile); + try { + return file.metadata().get(); + } finally { + file.close(); + } } /** @@ -138,12 +106,13 @@ public class ReplayFileIO { if(replayFile == lastReplayFile) { return lastContainsJoinPacket; } + ReplayFile file = null; lastReplayFile = replayFile; lastContainsJoinPacket = false; try { - Pair pair = getTempFileInputStream(replayFile); - dis = pair.second(); + file = new ReplayFile(replayFile); + dis = new DataInputStream(file.recording().get()); PacketData pd = readPacketData(dis); while(dis.available() > 0) { Packet p = deserializePacket(pd.getByteArray()); @@ -163,8 +132,10 @@ public class ReplayFileIO { if(dis != null) { dis.close(); } - } catch(Exception e) { - } + if (file != null) { + file.close(); + } + } catch(Exception ignored) {} } return false; @@ -238,6 +209,7 @@ public class ReplayFileIO { RandomAccessFile raf = null; DataInputStream dis = null; + ReplayFile file = null; boolean bounds = false; int lower = 0, upper = 0; @@ -255,10 +227,10 @@ public class ReplayFileIO { outputFile.createNewFile(); } raf = new RandomAccessFile(outputFile, "rw"); + file = new ReplayFile(replayFile); - Pair pair = getTempFileInputStream(replayFile); - dis = pair.second(); - long fileLength = pair.first(); + dis = new DataInputStream(file.recording().get()); + long fileLength = file.recordingEntry().getSize(); raf.setLength(fileLength); @@ -300,8 +272,10 @@ public class ReplayFileIO { if(dis != null) { dis.close(); } - } catch(Exception e) { - } + if(file != null) { + file.close(); + } + } catch(Exception ignored) {} } return false;