diff --git a/ReplayStudio b/ReplayStudio index a91987f7..3895d953 160000 --- a/ReplayStudio +++ b/ReplayStudio @@ -1 +1 @@ -Subproject commit a91987f72e68ca48eb8c5473e0682e2c94fdc5e1 +Subproject commit 3895d9535f204b0b8e42e5ee6981c92b873ccf70 diff --git a/jGui b/jGui index 96bdfcb7..611fa2a4 160000 --- a/jGui +++ b/jGui @@ -1 +1 @@ -Subproject commit 96bdfcb7d9a3f1b6e10c9a40a1ac2e215e3c1466 +Subproject commit 611fa2a4ef2a89848b1c452d5aae52fecd6bf1c8 diff --git a/src/main/java/com/replaymod/core/ReplayMod.java b/src/main/java/com/replaymod/core/ReplayMod.java index 9541f224..11e84591 100755 --- a/src/main/java/com/replaymod/core/ReplayMod.java +++ b/src/main/java/com/replaymod/core/ReplayMod.java @@ -1,8 +1,11 @@ package com.replaymod.core; +import com.google.common.io.Files; import com.google.common.util.concurrent.ListenableFutureTask; +import com.replaymod.core.gui.RestoreReplayGui; import com.replaymod.replay.ReplaySender; import com.replaymod.replaystudio.util.I18n; +import de.johni0702.minecraft.gui.container.GuiScreen; import eu.crushedpixel.replaymod.chat.ChatMessageHandler; import eu.crushedpixel.replaymod.events.handlers.CrosshairRenderHandler; import eu.crushedpixel.replaymod.events.handlers.GuiEventHandler; @@ -326,6 +329,23 @@ public class ReplayMod { testIfMoeshAndExitMinecraft(); } + + runLater(() -> { + // Restore corrupted replays + try { + File[] files = getReplayFolder().listFiles(); + if (files != null) { + 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(); + } + } + } + } catch (IOException e) { + e.printStackTrace(); + } + }); } public void runLater(Runnable runnable) { diff --git a/src/main/java/com/replaymod/core/gui/RestoreReplayGui.java b/src/main/java/com/replaymod/core/gui/RestoreReplayGui.java new file mode 100644 index 00000000..654754f0 --- /dev/null +++ b/src/main/java/com/replaymod/core/gui/RestoreReplayGui.java @@ -0,0 +1,86 @@ +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.replay.ReplayFile; +import com.replaymod.replaystudio.replay.ReplayMetaData; +import com.replaymod.replaystudio.replay.ZipReplayFile; +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.element.GuiButton; +import de.johni0702.minecraft.gui.element.GuiLabel; +import de.johni0702.minecraft.gui.layout.CustomLayout; +import de.johni0702.minecraft.gui.layout.HorizontalLayout; +import de.johni0702.minecraft.gui.layout.VerticalLayout; + +import java.io.File; +import java.io.IOException; + +public class RestoreReplayGui extends AbstractGuiScreen { + + public final GuiScreen parent; + public final File file; + public final GuiPanel textPanel = new GuiPanel().setLayout(new VerticalLayout().setSpacing(3)); + public final GuiPanel buttonPanel = new GuiPanel().setLayout(new HorizontalLayout().setSpacing(5)); + public final GuiPanel contentPanel = new GuiPanel(this).addElements(new VerticalLayout.Data(0.5), + 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"); + + public RestoreReplayGui(GuiScreen parent, File file) { + this.parent = parent; + this.file = file; + + textPanel.addElements(new VerticalLayout.Data(0.5), + new GuiLabel().setI18nText("replaymod.gui.restorereplay1"), + new GuiLabel().setI18nText("replaymod.gui.restorereplay2", Files.getNameWithoutExtension(file.getName())), + new GuiLabel().setI18nText("replaymod.gui.restorereplay3")); + yesButton.onClick(() -> { + try { + ReplayFile replayFile = new ZipReplayFile(new ReplayStudio(), null, file); + ReplayMetaData metaData = replayFile.getMetaData(); + if (metaData != null && metaData.getDuration() == 0) { + // Try to restore replay duration + try (ReplayInputStream in = replayFile.getPacketData()) { + PacketData last = null; + while ((last = in.readPacket()) != null) { + metaData.setDuration((int) last.getTime()); + } + replayFile.writeMetaData(metaData); + } catch (Throwable t) { + t.printStackTrace(); + } + } + replayFile.save(); + replayFile.close(); + } catch (IOException e) { + e.printStackTrace(); + } + parent.display(); + }); + noButton.onClick(() -> { + try { + Files.move(new File(file.getParentFile(), file.getName() + ".tmp"), + new File(file.getParentFile(), file.getName() + ".del")); + } catch (IOException e) { + e.printStackTrace(); + } + parent.display(); + }); + + setLayout(new CustomLayout() { + @Override + protected void layout(RestoreReplayGui container, int width, int height) { + pos(contentPanel, width / 2 - width(contentPanel) / 2, height / 2 - height(contentPanel) / 2); + } + }); + } + + @Override + protected RestoreReplayGui getThis() { + return this; + } +} diff --git a/src/main/java/com/replaymod/recording/handler/ConnectionEventHandler.java b/src/main/java/com/replaymod/recording/handler/ConnectionEventHandler.java index 77a3e15b..9735d6c0 100755 --- a/src/main/java/com/replaymod/recording/handler/ConnectionEventHandler.java +++ b/src/main/java/com/replaymod/recording/handler/ConnectionEventHandler.java @@ -5,6 +5,7 @@ import com.replaymod.recording.Setting; import com.replaymod.recording.gui.GuiRecordingOverlay; import com.replaymod.recording.packet.PacketListener; import com.replaymod.replaystudio.replay.ReplayFile; +import com.replaymod.replaystudio.replay.ReplayMetaData; import com.replaymod.replaystudio.replay.ZipReplayFile; import com.replaymod.replaystudio.studio.ReplayStudio; import io.netty.channel.Channel; @@ -79,7 +80,13 @@ public class ConnectionEventHandler { File currentFile = new File(folder, name + ".mcpr"); ReplayFile replayFile = new ZipReplayFile(new ReplayStudio(), currentFile); - packetListener = new PacketListener(replayFile, name, worldName, System.currentTimeMillis(), event.isLocal); + ReplayMetaData metaData = new ReplayMetaData(); + metaData.setSingleplayer(event.isLocal); + metaData.setServerName(worldName); + metaData.setGenerator("ReplayMod v" + ReplayMod.getContainer().getVersion()); + metaData.setDate(System.currentTimeMillis()); + metaData.setMcVersion(ReplayMod.getMinecraftVersion()); + packetListener = new PacketListener(replayFile, metaData); pipeline.addBefore(packetHandlerKey, "replay_recorder", packetListener); recordingEventHandler = new RecordingEventHandler(packetListener); diff --git a/src/main/java/com/replaymod/recording/handler/RecordingEventHandler.java b/src/main/java/com/replaymod/recording/handler/RecordingEventHandler.java index 4aafa8ee..f0e46d78 100755 --- a/src/main/java/com/replaymod/recording/handler/RecordingEventHandler.java +++ b/src/main/java/com/replaymod/recording/handler/RecordingEventHandler.java @@ -376,7 +376,7 @@ public class RecordingEventHandler { if (mc.isIntegratedServerRunning()) { IntegratedServer server = mc.getIntegratedServer(); if (server != null && server.isGamePaused) { - packetListener.serverWasPaused = true; + packetListener.setServerWasPaused(); } } } diff --git a/src/main/java/com/replaymod/recording/packet/DataListener.java b/src/main/java/com/replaymod/recording/packet/DataListener.java deleted file mode 100755 index fe48381b..00000000 --- a/src/main/java/com/replaymod/recording/packet/DataListener.java +++ /dev/null @@ -1,177 +0,0 @@ -package com.replaymod.recording.packet; - -import com.google.common.hash.Hashing; -import com.google.common.io.Files; -import com.replaymod.core.ReplayMod; -import com.replaymod.replaystudio.data.Marker; -import com.replaymod.replaystudio.replay.ReplayFile; -import com.replaymod.replaystudio.replay.ReplayMetaData; -import eu.crushedpixel.replaymod.holders.AdvancedPosition; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelInboundHandlerAdapter; -import net.minecraft.client.Minecraft; -import org.apache.commons.io.FileUtils; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import java.io.DataOutputStream; -import java.io.File; -import java.io.IOException; -import java.io.OutputStream; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; - -public abstract class DataListener extends ChannelInboundHandlerAdapter { - - private static final Logger logger = LogManager.getLogger(); - - private final ReplayFile replayFile; - protected final DataWriter dataWriter; - - protected Long startTime = null; - protected String name; - protected String worldName; - public boolean serverWasPaused; - protected long lastSentPacket = 0; - protected boolean alive = true; - protected Set players = new HashSet<>(); - private boolean singleplayer; - - private final Set markers = new HashSet<>(); - private final Map requestToHash = new ConcurrentHashMap<>(); - - public DataListener(ReplayFile replayFile, String name, String worldName, long startTime, boolean singleplayer) throws IOException { - this.replayFile = replayFile; - this.startTime = startTime; - this.name = name; - this.worldName = worldName; - this.singleplayer = singleplayer; - - dataWriter = new DataWriter(); - } - - @Override - public void channelInactive(ChannelHandlerContext ctx) { - dataWriter.requestFinish(); - } - - protected void recordResourcePack(File file, int requestId) { - try { - String hash = Hashing.sha1().hashBytes(Files.toByteArray(file)).toString(); - synchronized (dataWriter) { - if (!requestToHash.containsValue(hash)) { - try (OutputStream out = replayFile.writeResourcePack(hash)) { - FileUtils.copyFile(file, out); - } - } - } - requestToHash.put(requestId, hash); - } catch (IOException e) { - logger.warn("Failed to save resource pack.", e); - } - } - - public void addMarker() { - AdvancedPosition pos = new AdvancedPosition(Minecraft.getMinecraft().getRenderViewEntity()); - int timestamp = (int) (System.currentTimeMillis() - startTime); - - Marker marker = new Marker(); - marker.setTime(timestamp); - marker.setX(pos.getX()); - marker.setY(pos.getY()); - marker.setZ(pos.getZ()); - marker.setYaw((float) pos.getYaw()); - marker.setPitch((float) pos.getPitch()); - marker.setRoll((float) pos.getRoll()); - markers.add(marker); - } - - public class DataWriter { - private long paused; - - private DataOutputStream stream; - private ExecutorService saveService = Executors.newSingleThreadExecutor(); - - private final Thread shutdownHook = new Thread(new Runnable() { - @Override - public void run() { - if (!saveService.isShutdown()) { - requestFinish(); - } - } - }, "shutdown-hook-data-listener"); - - public DataWriter() throws IOException { - stream = new DataOutputStream(replayFile.writePacketData()); - Runtime.getRuntime().addShutdownHook(shutdownHook); - } - - public synchronized void writePacket(final byte[] bytes) { - long now = System.currentTimeMillis(); - if(startTime == null) { - startTime = now; - } - - if (serverWasPaused) { - paused = now - startTime - lastSentPacket; - serverWasPaused = false; - } - final int timestamp = (int) (now - startTime - paused); - lastSentPacket = timestamp; - saveService.submit(new Runnable() { - @Override - public void run() { - try { - stream.writeInt(timestamp); - stream.writeInt(bytes.length); - stream.write(bytes); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - }); - } - - public synchronized void requestFinish() { - if (saveService.isShutdown()) return; - // TODO: Add some GUI showing saving status - try { - Runtime.getRuntime().removeShutdownHook(shutdownHook); - saveService.shutdown(); - saveService.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS); - stream.close(); - - String mcversion = ReplayMod.getMinecraftVersion(); - - String[] pl = players.toArray(new String[players.size()]); - - String generator = "ReplayMod v" + ReplayMod.getContainer().getVersion(); - - ReplayMetaData metaData = new ReplayMetaData(); - metaData.setSingleplayer(singleplayer); - metaData.setServerName(worldName); - metaData.setGenerator(generator); - metaData.setDuration((int) lastSentPacket); - metaData.setDate(startTime); - metaData.setPlayers(pl); - metaData.setMcVersion(mcversion); - - replayFile.writeMetaData(metaData); - replayFile.writeMarkers(markers); - replayFile.writeResourcePackIndex(requestToHash); - replayFile.save(); - } catch(Exception e) { - e.printStackTrace(); - } finally { - // TODO: Always close GUI - } - } - - } - -} diff --git a/src/main/java/com/replaymod/recording/packet/PacketListener.java b/src/main/java/com/replaymod/recording/packet/PacketListener.java index a2e9dd43..25105bcb 100755 --- a/src/main/java/com/replaymod/recording/packet/PacketListener.java +++ b/src/main/java/com/replaymod/recording/packet/PacketListener.java @@ -1,64 +1,137 @@ package com.replaymod.recording.packet; -import com.google.common.hash.Hashing; -import com.google.common.io.Files; -import com.google.common.util.concurrent.FutureCallback; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.replaymod.replaystudio.replay.ReplayFile; import com.replaymod.core.utils.Restrictions; -import eu.crushedpixel.replaymod.utils.ReplayFileIO; +import com.replaymod.replaystudio.data.Marker; +import com.replaymod.replaystudio.replay.ReplayFile; +import com.replaymod.replaystudio.replay.ReplayMetaData; +import eu.crushedpixel.replaymod.holders.AdvancedPosition; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; 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; -import net.minecraft.client.resources.I18n; -import net.minecraft.client.resources.ResourcePackRepository; import net.minecraft.entity.DataWatcher; -import net.minecraft.network.NetworkManager; +import net.minecraft.network.EnumConnectionState; +import net.minecraft.network.EnumPacketDirection; import net.minecraft.network.Packet; -import net.minecraft.network.play.client.C19PacketResourcePackStatus; +import net.minecraft.network.PacketBuffer; import net.minecraft.network.play.server.*; import net.minecraft.util.ChatComponentText; -import net.minecraft.util.HttpUtil; -import org.apache.commons.io.FileUtils; +import net.minecraftforge.fml.common.network.internal.FMLProxyPacket; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import javax.annotation.Nonnull; -import java.io.File; +import java.io.DataOutputStream; import java.io.IOException; -import java.util.List; -import java.util.Map; -import java.util.UUID; +import java.util.*; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; -public class PacketListener extends DataListener { +public class PacketListener extends ChannelInboundHandlerAdapter { private static final Minecraft mc = Minecraft.getMinecraft(); private static final Logger logger = LogManager.getLogger(); + private final ReplayFile replayFile; + + private final ResourcePackRecorder resourcePackRecorder; + + private final ExecutorService saveService = Executors.newSingleThreadExecutor(); + private final DataOutputStream packetOutputStream; + + private ReplayMetaData metaData; + private ChannelHandlerContext context = null; - private int nextRequestId; + private final long startTime; + private long lastSentPacket; + private long timePassedWhilePaused; + private volatile boolean serverWasPaused; - public PacketListener(ReplayFile file, String name, String worldName, long startTime, boolean singleplayer) throws IOException { - super(file, name, worldName, startTime, singleplayer); + /** + * Used to keep track of the last metadata save job submitted to the save service and + * as such prevents unnecessary writes. + */ + private final AtomicInteger lastSaveMetaDataId = new AtomicInteger(); + + public PacketListener(ReplayFile replayFile, ReplayMetaData metaData) throws IOException { + this.replayFile = replayFile; + this.metaData = metaData; + this.resourcePackRecorder = new ResourcePackRecorder(replayFile); + this.packetOutputStream = new DataOutputStream(replayFile.writePacketData()); + this.startTime = metaData.getDate(); + + saveMetaData(); + } + + private void saveMetaData() { + int id = lastSaveMetaDataId.incrementAndGet(); + saveService.submit(() -> { + if (lastSaveMetaDataId.get() != id) { + return; // Another job has been scheduled, it will do the hard work. + } + try { + synchronized (replayFile) { + replayFile.writeMetaData(metaData); + } + } catch (IOException e) { + logger.error("Writing metadata:", e); + } + }); } public void save(Packet packet) { try { if(packet instanceof S0CPacketSpawnPlayer) { UUID uuid = ((S0CPacketSpawnPlayer) packet).func_179819_c(); - players.add(uuid.toString()); + Set uuids = new HashSet<>(Arrays.asList(metaData.getPlayers())); + uuids.add(uuid.toString()); + metaData.setPlayers(uuids.toArray(new String[uuids.size()])); + saveMetaData(); } - dataWriter.writePacket(getPacketData(packet)); + byte[] bytes = getPacketData(packet); + long now = System.currentTimeMillis(); + saveService.submit(() -> { + if (serverWasPaused) { + timePassedWhilePaused = now - startTime - lastSentPacket; + serverWasPaused = false; + } + int timestamp = (int) (now - startTime - timePassedWhilePaused); + lastSentPacket = timestamp; + try { + packetOutputStream.writeInt(timestamp); + packetOutputStream.writeInt(bytes.length); + packetOutputStream.write(bytes); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); } catch(Exception e) { - e.printStackTrace(); + logger.error("Writing packet:", e); + } + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) { + metaData.setDuration((int) lastSentPacket); + saveMetaData(); + + saveService.shutdown(); + try { + saveService.awaitTermination(10, TimeUnit.SECONDS); + } catch (InterruptedException e) { + logger.error("Waiting for save service termination:", e); + } + + synchronized (replayFile) { + try { + replayFile.save(); + } catch (IOException e) { + logger.error("Saving replay file:", e); + } } } @@ -72,11 +145,8 @@ public class PacketListener extends DataListener { } } this.context = ctx; - if(!alive) { - super.channelRead(ctx, msg); - return; - } - if(msg instanceof Packet) { + + if (msg instanceof Packet) { try { Packet packet = (Packet) msg; @@ -88,29 +158,28 @@ public class PacketListener extends DataListener { } } - if(packet instanceof S0CPacketSpawnPlayer) { - UUID uuid = ((S0CPacketSpawnPlayer) packet).func_179819_c(); - players.add(uuid.toString()); - } - if (packet instanceof S48PacketResourcePackSend) { - S48PacketResourcePackSend p = (S48PacketResourcePackSend) packet; - int requestId = handleResourcePack(p); - save(new S48PacketResourcePackSend("replay://" + requestId, "")); + save(resourcePackRecorder.handleResourcePack((S48PacketResourcePackSend) packet)); return; } - dataWriter.writePacket(getPacketData(packet)); + if (packet instanceof FMLProxyPacket) { + // This packet requires special handling + super.channelRead(ctx, msg); + return; + } + + save(packet); if (packet instanceof S3FPacketCustomPayload) { S3FPacketCustomPayload p = (S3FPacketCustomPayload) packet; if (Restrictions.PLUGIN_CHANNEL.equals(p.getChannelName())) { packet = new S40PacketDisconnect(new ChatComponentText("Please update to view this replay.")); - dataWriter.writePacket(getPacketData(packet)); + save(packet); } } } catch(Exception e) { - e.printStackTrace(); + logger.error("Handling packet for recording:", e); } } @@ -119,7 +188,7 @@ public class PacketListener extends DataListener { } @SuppressWarnings("unchecked") - private byte[] getPacketData(Packet packet) { + private byte[] getPacketData(Packet packet) throws IOException { if(packet instanceof S0FPacketSpawnMob) { S0FPacketSpawnMob p = (S0FPacketSpawnMob) packet; if (p.field_149043_l == null) { @@ -144,155 +213,49 @@ public class PacketListener extends DataListener { } } - return ReplayFileIO.serializePacket(packet); - } - - public synchronized int handleResourcePack(S48PacketResourcePackSend packet) { - final int requestId = nextRequestId++; - final NetHandlerPlayClient netHandler = mc.getNetHandler(); - final NetworkManager netManager = netHandler.getNetworkManager(); - final String url = packet.func_179783_a(); - final String hash = packet.func_179784_b(); - - if (url.startsWith("level://")) { - String levelName = url.substring("level://".length()); - File savesDir = new File(mc.mcDataDir, "saves"); - final File levelDir = new File(savesDir, levelName); - - if (levelDir.isFile()) { - netManager.sendPacket(new C19PacketResourcePackStatus(hash, C19PacketResourcePackStatus.Action.ACCEPTED)); - Futures.addCallback(mc.getResourcePackRepository().func_177319_a(levelDir), new FutureCallback() { - @Override - public void onSuccess(Object result) { - recordResourcePack(levelDir, requestId); - netManager.sendPacket(new C19PacketResourcePackStatus(hash, C19PacketResourcePackStatus.Action.SUCCESSFULLY_LOADED)); - } - - @Override - public void onFailure(@Nonnull Throwable throwable) { - netManager.sendPacket(new C19PacketResourcePackStatus(hash, C19PacketResourcePackStatus.Action.FAILED_DOWNLOAD)); - } - }); - } else { - netManager.sendPacket(new C19PacketResourcePackStatus(hash, C19PacketResourcePackStatus.Action.FAILED_DOWNLOAD)); - } - } else { - final ServerData serverData = mc.getCurrentServerData(); - if (serverData != null && serverData.getResourceMode() == ServerData.ServerResourceMode.ENABLED) { - netManager.sendPacket(new C19PacketResourcePackStatus(hash, C19PacketResourcePackStatus.Action.ACCEPTED)); - downloadResourcePackFuture(requestId, url, hash); - } else if (serverData != null && serverData.getResourceMode() != ServerData.ServerResourceMode.PROMPT) { - netManager.sendPacket(new C19PacketResourcePackStatus(hash, C19PacketResourcePackStatus.Action.DECLINED)); - } else { - mc.addScheduledTask(new Runnable() { - @Override - public void run() { - 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)); - downloadResourcePackFuture(requestId, url, hash); - } else { - netManager.sendPacket(new C19PacketResourcePackStatus(hash, C19PacketResourcePackStatus.Action.DECLINED)); - } - - ServerList.func_147414_b(serverData); - mc.displayGuiScreen(null); - } - }, I18n.format("multiplayer.texturePrompt.line1"), I18n.format("multiplayer.texturePrompt.line2"), 0)); - } - }); - } + Integer packetId = EnumConnectionState.PLAY.getPacketId(EnumPacketDirection.CLIENTBOUND, packet); + if (packetId == null) { + throw new IOException("Unknown packet type:" + packet.getClass()); } - return requestId; + ByteBuf byteBuf = Unpooled.buffer(); + PacketBuffer packetBuffer = new PacketBuffer(byteBuf); + packetBuffer.writeVarIntToBuffer(packetId); + packet.writePacketData(packetBuffer); + + byteBuf.readerIndex(0); + byte[] array = new byte[byteBuf.readableBytes()]; + byteBuf.readBytes(array); + + byteBuf.release(); + return array; } - private void downloadResourcePackFuture(int requestId, String url, final String hash) { - Futures.addCallback(downloadResourcePack(requestId, url, hash), new FutureCallback() { - @Override - public void onSuccess(Object result) { - mc.getNetHandler().addToSendQueue(new C19PacketResourcePackStatus(hash, C19PacketResourcePackStatus.Action.SUCCESSFULLY_LOADED)); - } + public void addMarker() { + AdvancedPosition pos = new AdvancedPosition(Minecraft.getMinecraft().getRenderViewEntity()); + int timestamp = (int) (System.currentTimeMillis() - startTime); - @Override - public void onFailure(@Nonnull Throwable throwable) { - mc.getNetHandler().addToSendQueue(new C19PacketResourcePackStatus(hash, C19PacketResourcePackStatus.Action.FAILED_DOWNLOAD)); + Marker marker = new Marker(); + marker.setTime(timestamp); + marker.setX(pos.getX()); + marker.setY(pos.getY()); + marker.setZ(pos.getZ()); + marker.setYaw((float) pos.getYaw()); + marker.setPitch((float) pos.getPitch()); + marker.setRoll((float) pos.getRoll()); + saveService.submit(() -> { + synchronized (replayFile) { + try { + Set markers = replayFile.getMarkers().or(HashSet::new); + markers.add(marker); + replayFile.writeMarkers(markers); + } catch (IOException e) { + logger.error("Writing markers:", e); + } } }); } - private ListenableFuture downloadResourcePack(final int requestId, String url, String hash) { - final ResourcePackRepository repo = mc.mcResourcePackRepository; - String fileName; - if (hash.matches("^[a-f0-9]{40}$")) { - fileName = hash; - } else { - fileName = url.substring(url.lastIndexOf("/") + 1); - - if (fileName.contains("?")) { - fileName = fileName.substring(0, fileName.indexOf("?")); - } - - if (!fileName.endsWith(".zip")) { - return Futures.immediateFailedFuture(new IllegalArgumentException("Invalid filename; must end in .zip")); - } - - fileName = "legacy_" + fileName.replaceAll("\\W", ""); - } - - final File file = new File(repo.dirServerResourcepacks, fileName); - repo.field_177321_h.lock(); - try { - repo.func_148529_f(); - - if (file.exists() && hash.length() == 40) { - try { - String fileHash = Hashing.sha1().hashBytes(Files.toByteArray(file)).toString(); - if (fileHash.equals(hash)) { - recordResourcePack(file, requestId); - return repo.func_177319_a(file); - } - - logger.warn("File " + file + " had wrong hash (expected " + hash + ", found " + fileHash + "). Deleting it."); - FileUtils.deleteQuietly(file); - } catch (IOException ioexception) { - logger.warn("File " + file + " couldn\'t be hashed. Deleting it.", ioexception); - FileUtils.deleteQuietly(file); - } - } - - final GuiScreenWorking guiScreen = new GuiScreenWorking(); - final Minecraft mc = Minecraft.getMinecraft(); - - Futures.getUnchecked(mc.addScheduledTask(new Runnable() { - @Override - public void run() { - mc.displayGuiScreen(guiScreen); - } - })); - - Map sessionInfo = Minecraft.getSessionInfo(); - repo.field_177322_i = HttpUtil.func_180192_a(file, url, sessionInfo, 50 * 1024 * 1024, guiScreen, mc.getProxy()); - Futures.addCallback(repo.field_177322_i, new FutureCallback() { - @Override - public void onSuccess(Object value) { - recordResourcePack(file, requestId); - repo.func_177319_a(file); - } - - @Override - public void onFailure(@Nonnull Throwable throwable) { - throwable.printStackTrace(); - } - }); - return repo.field_177322_i; - } finally { - repo.field_177321_h.unlock(); - } + public void setServerWasPaused() { + this.serverWasPaused = true; } - } diff --git a/src/main/java/com/replaymod/recording/packet/PacketSerializer.java b/src/main/java/com/replaymod/recording/packet/PacketSerializer.java deleted file mode 100755 index 12b3f214..00000000 --- a/src/main/java/com/replaymod/recording/packet/PacketSerializer.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.replaymod.recording.packet; - -import io.netty.buffer.ByteBuf; -import io.netty.channel.ChannelHandlerContext; -import net.minecraft.network.*; -import net.minecraft.util.MessageSerializer; - -import java.io.IOException; - -public class PacketSerializer extends MessageSerializer { - - public PacketSerializer(EnumPacketDirection direction) { - super(direction); - } - - @Override - public void encode(ChannelHandlerContext ctx, Packet packet, ByteBuf byteBuf) throws IOException { - EnumConnectionState state = ((EnumConnectionState) ctx.channel().attr(NetworkManager.attrKeyConnectionState).get()); - encode(state, packet, byteBuf); - } - - public void encode(EnumConnectionState state, Packet packet, ByteBuf byteBuf) { - Integer integer = state.getPacketId(EnumPacketDirection.CLIENTBOUND, packet); - - if (integer != null) { - PacketBuffer packetbuffer = new PacketBuffer(byteBuf); - packetbuffer.writeVarIntToBuffer(integer); - - try { - packet.writePacketData(packetbuffer); - } catch(Throwable throwable) { - throwable.printStackTrace(); - } - } - } - - -} diff --git a/src/main/java/com/replaymod/recording/packet/ResourcePackRecorder.java b/src/main/java/com/replaymod/recording/packet/ResourcePackRecorder.java new file mode 100644 index 00000000..a00ff274 --- /dev/null +++ b/src/main/java/com/replaymod/recording/packet/ResourcePackRecorder.java @@ -0,0 +1,209 @@ +package com.replaymod.recording.packet; + +import com.google.common.hash.Hashing; +import com.google.common.io.Files; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +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.multiplayer.ServerData; +import net.minecraft.client.multiplayer.ServerList; +import net.minecraft.client.network.NetHandlerPlayClient; +import net.minecraft.client.resources.I18n; +import net.minecraft.client.resources.ResourcePackRepository; +import net.minecraft.network.NetworkManager; +import net.minecraft.network.play.client.C19PacketResourcePackStatus; +import net.minecraft.network.play.server.S48PacketResourcePackSend; +import net.minecraft.util.HttpUtil; +import org.apache.commons.io.FileUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import javax.annotation.Nonnull; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Map; + +/** + * Records resource packs and handles incoming resource pack packets during recording. + */ +public class ResourcePackRecorder { + private static final Logger logger = LogManager.getLogger(); + private static final Minecraft mc = Minecraft.getMinecraft(); + + private final ReplayFile replayFile; + + private int nextRequestId; + + public ResourcePackRecorder(ReplayFile replayFile) { + this.replayFile = replayFile; + } + + public void recordResourcePack(File file, int requestId) { + try { + // Read in resource pack file + byte[] bytes = Files.toByteArray(file); + // Check whether it is already known + String hash = Hashing.sha1().hashBytes(bytes).toString(); + 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.containsValue(hash)) { + // Hash is unknown, we have to write the resource pack ourselves + doWrite = true; + } + // Save this request + index.put(requestId, hash); + replayFile.writeResourcePackIndex(index); + } + if (doWrite) { + try (OutputStream out = replayFile.writeResourcePack(hash)) { + out.write(bytes); + } + } + } catch (IOException e) { + logger.warn("Failed to save resource pack.", e); + } + } + + public synchronized S48PacketResourcePackSend handleResourcePack(S48PacketResourcePackSend packet) { + final int requestId = nextRequestId++; + final NetHandlerPlayClient netHandler = mc.getNetHandler(); + final NetworkManager netManager = netHandler.getNetworkManager(); + final String url = packet.func_179783_a(); + final String hash = packet.func_179784_b(); + + if (url.startsWith("level://")) { + String levelName = url.substring("level://".length()); + File savesDir = new File(mc.mcDataDir, "saves"); + final File levelDir = new File(savesDir, levelName); + + if (levelDir.isFile()) { + netManager.sendPacket(new C19PacketResourcePackStatus(hash, C19PacketResourcePackStatus.Action.ACCEPTED)); + Futures.addCallback(mc.getResourcePackRepository().func_177319_a(levelDir), new FutureCallback() { + @Override + public void onSuccess(Object result) { + recordResourcePack(levelDir, requestId); + netManager.sendPacket(new C19PacketResourcePackStatus(hash, C19PacketResourcePackStatus.Action.SUCCESSFULLY_LOADED)); + } + + @Override + public void onFailure(@Nonnull Throwable throwable) { + netManager.sendPacket(new C19PacketResourcePackStatus(hash, C19PacketResourcePackStatus.Action.FAILED_DOWNLOAD)); + } + }); + } else { + netManager.sendPacket(new C19PacketResourcePackStatus(hash, C19PacketResourcePackStatus.Action.FAILED_DOWNLOAD)); + } + } else { + final ServerData serverData = mc.getCurrentServerData(); + if (serverData != null && serverData.getResourceMode() == ServerData.ServerResourceMode.ENABLED) { + netManager.sendPacket(new C19PacketResourcePackStatus(hash, C19PacketResourcePackStatus.Action.ACCEPTED)); + downloadResourcePackFuture(requestId, url, hash); + } 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)); + } + + ServerList.func_147414_b(serverData); + mc.displayGuiScreen(null); + }, I18n.format("multiplayer.texturePrompt.line1"), I18n.format("multiplayer.texturePrompt.line2"), 0))); + } + } + + return new S48PacketResourcePackSend("replay://" + requestId, ""); + } + + private void downloadResourcePackFuture(int requestId, String url, final String hash) { + Futures.addCallback(downloadResourcePack(requestId, url, hash), new FutureCallback() { + @Override + public void onSuccess(Object result) { + mc.getNetHandler().addToSendQueue(new C19PacketResourcePackStatus(hash, C19PacketResourcePackStatus.Action.SUCCESSFULLY_LOADED)); + } + + @Override + public void onFailure(@Nonnull Throwable throwable) { + mc.getNetHandler().addToSendQueue(new C19PacketResourcePackStatus(hash, C19PacketResourcePackStatus.Action.FAILED_DOWNLOAD)); + } + }); + } + + private ListenableFuture downloadResourcePack(final int requestId, String url, String hash) { + final ResourcePackRepository repo = mc.mcResourcePackRepository; + String fileName; + if (hash.matches("^[a-f0-9]{40}$")) { + fileName = hash; + } else { + fileName = url.substring(url.lastIndexOf("/") + 1); + + if (fileName.contains("?")) { + fileName = fileName.substring(0, fileName.indexOf("?")); + } + + if (!fileName.endsWith(".zip")) { + return Futures.immediateFailedFuture(new IllegalArgumentException("Invalid filename; must end in .zip")); + } + + fileName = "legacy_" + fileName.replaceAll("\\W", ""); + } + + final File file = new File(repo.dirServerResourcepacks, fileName); + repo.field_177321_h.lock(); + try { + repo.func_148529_f(); + + if (file.exists() && hash.length() == 40) { + try { + String fileHash = Hashing.sha1().hashBytes(Files.toByteArray(file)).toString(); + if (fileHash.equals(hash)) { + recordResourcePack(file, requestId); + return repo.func_177319_a(file); + } + + logger.warn("File " + file + " had wrong hash (expected " + hash + ", found " + fileHash + "). Deleting it."); + FileUtils.deleteQuietly(file); + } catch (IOException ioexception) { + logger.warn("File " + file + " couldn\'t be hashed. Deleting it.", ioexception); + FileUtils.deleteQuietly(file); + } + } + + final GuiScreenWorking guiScreen = new GuiScreenWorking(); + final Minecraft mc = Minecraft.getMinecraft(); + + Futures.getUnchecked(mc.addScheduledTask(() -> mc.displayGuiScreen(guiScreen))); + + Map sessionInfo = Minecraft.getSessionInfo(); + repo.field_177322_i = HttpUtil.func_180192_a(file, url, sessionInfo, 50 * 1024 * 1024, guiScreen, mc.getProxy()); + Futures.addCallback(repo.field_177322_i, new FutureCallback() { + @Override + public void onSuccess(Object value) { + recordResourcePack(file, requestId); + repo.func_177319_a(file); + } + + @Override + public void onFailure(@Nonnull Throwable throwable) { + throwable.printStackTrace(); + } + }); + return repo.field_177322_i; + } finally { + repo.field_177321_h.unlock(); + } + } + +} diff --git a/src/main/java/eu/crushedpixel/replaymod/utils/ReplayFileIO.java b/src/main/java/eu/crushedpixel/replaymod/utils/ReplayFileIO.java index a15c26fb..f7ae6190 100755 --- a/src/main/java/eu/crushedpixel/replaymod/utils/ReplayFileIO.java +++ b/src/main/java/eu/crushedpixel/replaymod/utils/ReplayFileIO.java @@ -1,9 +1,8 @@ package eu.crushedpixel.replaymod.utils; import com.google.gson.Gson; -import com.replaymod.recording.packet.PacketSerializer; -import com.replaymod.replaystudio.replay.ReplayMetaData; import com.replaymod.core.ReplayMod; +import com.replaymod.replaystudio.replay.ReplayMetaData; import eu.crushedpixel.replaymod.assets.CustomObjectRepository; import eu.crushedpixel.replaymod.holders.PacketData; import io.netty.buffer.ByteBuf; @@ -27,7 +26,6 @@ import java.util.zip.ZipOutputStream; @SuppressWarnings("resource") //Gets handled by finalizer public class ReplayFileIO { - private static final PacketSerializer packetSerializer = new PacketSerializer(EnumPacketDirection.CLIENTBOUND); private static final byte[] uniqueBytes = new byte[]{0, 1, 1, 2, 3, 5, 8}; public static File getRenderFolder() throws IOException { @@ -98,18 +96,6 @@ public class ReplayFileIO { } } - public static byte[] serializePacket(Packet packet) { - ByteBuf bb = Unpooled.buffer(); - packetSerializer.encode(EnumConnectionState.PLAY, packet, bb); - bb.readerIndex(0); - byte[] array = new byte[bb.readableBytes()]; - bb.readBytes(array); - - bb.readerIndex(0); - - return array; - } - private static final Gson gson = new Gson(); private static void write(Object obj, File file) throws IOException { diff --git a/src/main/resources/assets/replaymod/lang/en_US.lang b/src/main/resources/assets/replaymod/lang/en_US.lang index 08c15a49..5a5ad57e 100644 --- a/src/main/resources/assets/replaymod/lang/en_US.lang +++ b/src/main/resources/assets/replaymod/lang/en_US.lang @@ -121,6 +121,10 @@ replaymod.gui.settings=Settings replaymod.gui.keyframerepo.delete=Are you sure you want to delete these keyframes? +replaymod.gui.restorereplay1=It seems like Minecraft has not quit normally. +replaymod.gui.restorereplay2=The Replay "%1$s" was not saved correctly. +replaymod.gui.restorereplay3=Do you wish to recover it? + #Only change these if it's neccessary replaymod.gui.replayviewer=Replay Viewer replaymod.gui.replaycenter=Replay Center