diff --git a/src/main/java/eu/crushedpixel/replaymod/recording/DataListener.java b/src/main/java/eu/crushedpixel/replaymod/recording/DataListener.java index 26996c47..2ac0b7f4 100755 --- a/src/main/java/eu/crushedpixel/replaymod/recording/DataListener.java +++ b/src/main/java/eu/crushedpixel/replaymod/recording/DataListener.java @@ -1,5 +1,7 @@ package eu.crushedpixel.replaymod.recording; +import com.google.common.hash.Hashing; +import com.google.common.io.Files; import com.google.gson.Gson; import eu.crushedpixel.replaymod.gui.GuiReplaySaving; import eu.crushedpixel.replaymod.holders.PacketData; @@ -8,16 +10,20 @@ import eu.crushedpixel.replaymod.utils.ReplayFileIO; 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.*; -import java.util.HashMap; -import java.util.HashSet; +import java.util.*; import java.util.Map.Entry; -import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; public abstract class DataListener extends ChannelInboundHandlerAdapter { + private static final Logger logger = LogManager.getLogger(); + protected File file; protected Long startTime = null; protected String name; @@ -29,6 +35,10 @@ public abstract class DataListener extends ChannelInboundHandlerAdapter { private boolean singleplayer; private Gson gson = new Gson(); + private final File tempResourcePacksFolder = Files.createTempDir(); + private final Map requestToHash = new ConcurrentHashMap(); + private final Map resourcePacks = new HashMap(); + public DataListener(File file, String name, String worldName, long startTime, boolean singleplayer) throws FileNotFoundException { this.file = file; this.startTime = startTime; @@ -54,6 +64,22 @@ public abstract class DataListener extends ChannelInboundHandlerAdapter { dataWriter.requestFinish(players); } + protected void recordResourcePack(File file, int requestId) { + try { + String hash = Hashing.sha1().hashBytes(Files.toByteArray(file)).toString(); + requestToHash.put(requestId, hash); + synchronized (resourcePacks) { + if (!resourcePacks.containsKey(hash)) { + File tempFile = new File(tempResourcePacksFolder, hash); + FileUtils.copyFile(file, tempFile); + resourcePacks.put(hash, tempFile); + } + } + } catch (IOException e) { + logger.warn("Failed to save resource pack.", e); + } + } + public class DataWriter { private boolean active = true; @@ -138,9 +164,10 @@ public abstract class DataListener extends ChannelInboundHandlerAdapter { File archive = new File(folder, name + ReplayFile.ZIP_FILE_EXTENSION); archive.createNewFile(); - ReplayFileIO.writeReplayFile(archive, file, metaData); + ReplayFileIO.writeReplayFile(archive, file, metaData, resourcePacks, requestToHash); file.delete(); + FileUtils.deleteDirectory(tempResourcePacksFolder); GuiReplaySaving.replaySaving = false; } catch(Exception e) { diff --git a/src/main/java/eu/crushedpixel/replaymod/recording/PacketListener.java b/src/main/java/eu/crushedpixel/replaymod/recording/PacketListener.java index de7c0de7..03b9eff0 100755 --- a/src/main/java/eu/crushedpixel/replaymod/recording/PacketListener.java +++ b/src/main/java/eu/crushedpixel/replaymod/recording/PacketListener.java @@ -1,27 +1,50 @@ package eu.crushedpixel.replaymod.recording; +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 eu.crushedpixel.replaymod.holders.PacketData; import eu.crushedpixel.replaymod.utils.ReplayFileIO; import io.netty.channel.ChannelHandlerContext; 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.Packet; +import net.minecraft.network.play.client.C19PacketResourcePackStatus; import net.minecraft.network.play.server.S0CPacketSpawnPlayer; import net.minecraft.network.play.server.S0DPacketCollectItem; import net.minecraft.network.play.server.S0FPacketSpawnMob; +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 java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.util.List; +import java.util.Map; import java.util.UUID; public class PacketListener extends DataListener { private static final Minecraft mc = Minecraft.getMinecraft(); + private static final Logger logger = LogManager.getLogger(); private ChannelHandlerContext context = null; + private int nextRequestId; public PacketListener(File file, String name, String worldName, long startTime, boolean singleplayer) throws FileNotFoundException { super(file, name, worldName, startTime, singleplayer); @@ -34,7 +57,7 @@ public class PacketListener extends DataListener { players.add(uuid.toString()); } - PacketData pd = getPacketData(context, packet); + PacketData pd = getPacketData(packet); writeData(pd); } catch(Exception e) { e.printStackTrace(); @@ -72,7 +95,14 @@ public class PacketListener extends DataListener { players.add(uuid.toString()); } - PacketData pd = getPacketData(ctx, packet); + if (packet instanceof S48PacketResourcePackSend) { + S48PacketResourcePackSend p = (S48PacketResourcePackSend) packet; + int requestId = handleResourcePack(p); + saveOnly(new S48PacketResourcePackSend("replay://" + requestId, "")); + return; + } + + PacketData pd = getPacketData(packet); writeData(pd); } catch(Exception e) { e.printStackTrace(); @@ -89,7 +119,7 @@ public class PacketListener extends DataListener { } @SuppressWarnings("unchecked") - private PacketData getPacketData(ChannelHandlerContext ctx, Packet packet) throws IOException, IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException { + private PacketData getPacketData(Packet packet) throws IOException { if(startTime == null) startTime = System.currentTimeMillis(); @@ -120,10 +150,148 @@ public class PacketListener extends DataListener { } } - byte[] array = ReplayFileIO.serializePacket(packet); return new PacketData(array, timestamp); } + 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() { + public void onSuccess(Object result) { + recordResourcePack(levelDir, requestId); + netManager.sendPacket(new C19PacketResourcePackStatus(hash, C19PacketResourcePackStatus.Action.SUCCESSFULLY_LOADED)); + } + + public void onFailure(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() { + public void run() { + mc.displayGuiScreen(new GuiYesNo(new GuiYesNoCallback() { + 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)); + } + }); + } + } + return requestId; + } + + private void downloadResourcePackFuture(int requestId, String url, final String hash) { + Futures.addCallback(downloadResourcePack(requestId, url, hash), new FutureCallback() { + public void onSuccess(Object result) { + mc.getNetHandler().addToSendQueue(new C19PacketResourcePackStatus(hash, C19PacketResourcePackStatus.Action.SUCCESSFULLY_LOADED)); + } + + public void onFailure(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(new Runnable() { + 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() { + public void onSuccess(Object value) { + recordResourcePack(file, requestId); + repo.func_177319_a(file); + } + + public void onFailure(Throwable throwable) { + throwable.printStackTrace(); + } + }); + return repo.field_177322_i; + } finally { + repo.field_177321_h.unlock(); + } + } + } diff --git a/src/main/java/eu/crushedpixel/replaymod/replay/ReplaySender.java b/src/main/java/eu/crushedpixel/replaymod/replay/ReplaySender.java index 54a23a97..cd695834 100755 --- a/src/main/java/eu/crushedpixel/replaymod/replay/ReplaySender.java +++ b/src/main/java/eu/crushedpixel/replaymod/replay/ReplaySender.java @@ -1,6 +1,7 @@ package eu.crushedpixel.replaymod.replay; import com.google.common.base.Preconditions; +import com.google.common.io.Files; import com.google.common.util.concurrent.ListenableFutureTask; import eu.crushedpixel.replaymod.ReplayMod; import eu.crushedpixel.replaymod.entities.CameraEntity; @@ -15,7 +16,6 @@ import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiDownloadTerrain; -import net.minecraft.client.resources.ResourcePackRepository; import net.minecraft.network.EnumConnectionState; import net.minecraft.network.NetworkManager; import net.minecraft.network.Packet; @@ -24,14 +24,12 @@ import net.minecraft.world.EnumDifficulty; import net.minecraft.world.WorldSettings.GameType; import net.minecraft.world.WorldType; import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; -import java.io.DataInputStream; -import java.io.EOFException; -import java.io.File; -import java.io.IOException; -import java.net.URL; +import java.io.*; import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.concurrent.Callable; /** @@ -137,6 +135,11 @@ public class ReplaySender extends ChannelInboundHandlerAdapter { */ protected boolean allowMovement; + /** + * Directory to which resource packs are extracted. + */ + private final File tempResourcePackFolder = Files.createTempDir(); + /** * Create a new replay sender. * @param file The replay file @@ -249,7 +252,7 @@ public class ReplaySender extends ChannelInboundHandlerAdapter { * @param p The packet to process * @return The processed packet or {@code null} if no packet shall be sent */ - protected Packet processPacket(Packet p) { + protected Packet processPacket(Packet p) throws Exception { if(BAD_PACKETS.contains(p.getClass())) return null; if(p instanceof S29PacketSoundEffect && ReplayProcess.isVideoRecording()) { @@ -262,7 +265,21 @@ public class ReplaySender extends ChannelInboundHandlerAdapter { if(p instanceof S48PacketResourcePackSend) { S48PacketResourcePackSend packet = (S48PacketResourcePackSend) p; - new ResourcePackCheck(packet.func_179783_a(), packet.func_179784_b()).start(); + String url = packet.func_179783_a(); + if (url.startsWith("replay://")) { + int id = Integer.parseInt(url.substring("replay://".length())); + Map index = replayFile.resourcePackIndex().get(); + if (index != null) { + String hash = index.get(id); + if (hash != null) { + File file = new File(tempResourcePackFolder, hash + ".zip"); + if (!file.exists()) { + IOUtils.copy(replayFile.resourcePack(hash).get(), new FileOutputStream(file)); + } + mc.getResourcePackRepository().func_177319_a(file); + } + } + } return null; } @@ -355,6 +372,7 @@ public class ReplaySender extends ChannelInboundHandlerAdapter { @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { replayFile.close(); + FileUtils.deleteDirectory(tempResourcePackFolder); super.channelInactive(ctx); } @@ -386,102 +404,6 @@ public class ReplaySender extends ChannelInboundHandlerAdapter { MCTimerHandler.setTimerSpeed((float) d); } - /** - * 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(); - private static ResourcePackRepository repo = mc.getResourcePackRepository(); - - private String url, hash; - - public ResourcePackCheck(String url, String hash) { - this.url = url; - this.hash = hash; - } - - /** - * 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; - - 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 null; - } - - filename = "legacy_" + filename.replaceAll("\\W", ""); - } - - 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); - return true; - } catch(Exception e) { - e.printStackTrace(); - } - 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.replaySettings.getUseResourcePacks(); - if(!use) return; - - System.out.println("Looking for downloaded Resource Pack..."); - File rp = getServerResourcePackLocation(url, hash); - if(rp == null) { - System.out.println("Invalid Resource Pack provided"); - return; - } - if(rp.exists()) { - System.out.println("Resource Pack found!"); - repo.func_177319_a(rp); - - } else { - System.out.println("No Resource Pack found."); - System.out.println("Attempting to download Resource Pack..."); - boolean success = downloadServerResourcePack(url, rp); - System.out.println(success ? "Resource pack was successfully downloaded!" : "Resource Pack download failed."); - if(success) { - repo.func_177319_a(rp); - } - } - - } catch(Exception e) { - e.printStackTrace(); - } - } - } - ///////////////////////////////////////////////////////// // Asynchronous packet processing // ///////////////////////////////////////////////////////// diff --git a/src/main/java/eu/crushedpixel/replaymod/studio/StudioImplementation.java b/src/main/java/eu/crushedpixel/replaymod/studio/StudioImplementation.java index a1f2bed4..7da83e9b 100755 --- a/src/main/java/eu/crushedpixel/replaymod/studio/StudioImplementation.java +++ b/src/main/java/eu/crushedpixel/replaymod/studio/StudioImplementation.java @@ -1,5 +1,6 @@ package eu.crushedpixel.replaymod.studio; +import com.google.common.io.Files; import com.google.gson.JsonObject; import de.johni0702.replaystudio.PacketData; import de.johni0702.replaystudio.filter.ChangeTimestampFilter; @@ -9,13 +10,17 @@ import de.johni0702.replaystudio.io.ReplayOutputStream; import de.johni0702.replaystudio.stream.PacketStream; import de.johni0702.replaystudio.studio.ReplayStudio; import eu.crushedpixel.replaymod.recording.ReplayMetaData; +import eu.crushedpixel.replaymod.utils.ReplayFile; import eu.crushedpixel.replaymod.utils.ReplayFileIO; +import org.apache.commons.io.IOUtils; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; +import java.util.HashMap; import java.util.List; +import java.util.Map; public class StudioImplementation { @@ -49,13 +54,25 @@ public class StudioImplementation { out.close(); - ReplayMetaData metaData = ReplayFileIO.getMetaData(replayFile); + ReplayFile replay = new ReplayFile(replayFile); + ReplayMetaData metaData = replay.metadata().get(); ending = Math.min(metaData.getDuration(), ending); metaData.setDuration(ending - beginning); + Map resourcePackIndex = replay.resourcePackIndex().get(); + Map resourcePacks = new HashMap(); + File tempFolder = Files.createTempDir(); + for (String hash : resourcePackIndex.values()) { + if (!resourcePacks.containsKey(hash)) { + File resourcePack = new File(tempFolder, hash); + IOUtils.copy(replay.resourcePack(hash).get(), new FileOutputStream(resourcePack)); + resourcePacks.put(hash, resourcePack); + } + } + outputFile.createNewFile(); - ReplayFileIO.writeReplayFile(outputFile, temp, metaData); + ReplayFileIO.writeReplayFile(outputFile, temp, metaData, resourcePacks, resourcePackIndex); } //TODO Work with Johni to connect multiple Replay Files diff --git a/src/main/java/eu/crushedpixel/replaymod/utils/ReplayFile.java b/src/main/java/eu/crushedpixel/replaymod/utils/ReplayFile.java index ec73310d..171de703 100644 --- a/src/main/java/eu/crushedpixel/replaymod/utils/ReplayFile.java +++ b/src/main/java/eu/crushedpixel/replaymod/utils/ReplayFile.java @@ -2,6 +2,8 @@ package eu.crushedpixel.replaymod.utils; import com.google.common.base.Supplier; import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; import eu.crushedpixel.replaymod.holders.KeyframeSet; import eu.crushedpixel.replaymod.holders.PlayerVisibility; import eu.crushedpixel.replaymod.recording.ReplayMetaData; @@ -11,6 +13,8 @@ import org.apache.commons.compress.archivers.zip.ZipFile; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.*; +import java.util.HashMap; +import java.util.Map; public class ReplayFile extends ZipFile { @@ -21,6 +25,8 @@ public class ReplayFile extends ZipFile { public static final String ENTRY_METADATA = "metaData" + JSON_FILE_EXTENSION; public static final String ENTRY_PATHS = "paths"; public static final String ENTRY_THUMB = "thumb"; + public static final String ENTRY_RESOURCE_PACK = "resourcepack/%s.zip"; + public static final String ENTRY_RESOURCE_PACK_INDEX = "resourcepack/index.json"; public static final String ENTRY_VISIBILITY = "visibility"; private final File file; @@ -138,4 +144,52 @@ public class ReplayFile extends ZipFile { } }; } + + public ZipArchiveEntry resourcePackIndexEntry() { + return getEntry(ENTRY_RESOURCE_PACK_INDEX); + } + + public Supplier> resourcePackIndex() { + return new Supplier>() { + @Override + @SuppressWarnings("unchecked") + public Map get() { + try { + ZipArchiveEntry entry = resourcePackIndexEntry(); + if (entry == null) { + return null; + } + Map index = new HashMap(); + JsonObject json = new Gson().fromJson(new InputStreamReader(getInputStream(entry)), JsonObject.class); + for (Map.Entry e : json.entrySet()) { + index.put(Integer.parseInt(e.getKey()), e.getValue().getAsString()); + } + return index; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + }; + } + + public ZipArchiveEntry resourcePackEntry(String hash) { + return getEntry(String.format(ENTRY_RESOURCE_PACK, hash)); + } + + public Supplier resourcePack(final String hash) { + return new Supplier() { + @Override + public InputStream get() { + try { + ZipArchiveEntry entry = resourcePackEntry(hash); + if (entry == null) { + return null; + } + return getInputStream(entry); + } 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 e38cd1b8..7048226f 100755 --- a/src/main/java/eu/crushedpixel/replaymod/utils/ReplayFileIO.java +++ b/src/main/java/eu/crushedpixel/replaymod/utils/ReplayFileIO.java @@ -16,12 +16,14 @@ import net.minecraft.network.PacketBuffer; import net.minecraft.network.play.server.S01PacketJoinGame; import net.minecraft.network.play.server.S08PacketPlayerPosLook; import org.apache.commons.io.FilenameUtils; +import org.apache.commons.io.IOUtils; import java.io.*; import java.nio.file.Files; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Map; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import java.util.zip.ZipOutputStream; @@ -58,7 +60,8 @@ public class ReplayFileIO { return files; } - public static void writeReplayFile(File replayFile, File tempFile, ReplayMetaData metaData) throws IOException { + public static void writeReplayFile(File replayFile, File tempFile, ReplayMetaData metaData, + Map resourcePacks, Map resourcePackRequests) throws IOException { byte[] buffer = new byte[1024]; if(!replayFile.exists()) { @@ -86,6 +89,20 @@ public class ReplayFileIO { fis.close(); zos.closeEntry(); + if (!resourcePackRequests.isEmpty()) { + zos.putNextEntry(new ZipEntry(ReplayFile.ENTRY_RESOURCE_PACK_INDEX)); + pw = new PrintWriter(zos); + pw.write(new Gson().toJson(resourcePackRequests)); + pw.flush(); + zos.closeEntry(); + + for (Map.Entry entry : resourcePacks.entrySet()) { + zos.putNextEntry(new ZipEntry(String.format(ReplayFile.ENTRY_RESOURCE_PACK, entry.getKey()))); + IOUtils.copy(new FileInputStream(entry.getValue()), zos); + zos.closeEntry(); + } + } + zos.close(); } diff --git a/src/main/resources/META-INF/replaymod_at.cfg b/src/main/resources/META-INF/replaymod_at.cfg index 98a60ba8..d2a39c14 100644 --- a/src/main/resources/META-INF/replaymod_at.cfg +++ b/src/main/resources/META-INF/replaymod_at.cfg @@ -9,6 +9,7 @@ public net.minecraft.client.Minecraft field_71429_W # leftClickCounter public net.minecraft.client.Minecraft field_71445_n # isGamePaused public net.minecraft.client.Minecraft field_152351_aB # scheduledTasks public net.minecraft.client.Minecraft field_110449_ao # defaultResourcePacks +public net.minecraft.client.Minecraft field_110448_aq # mcResourcePackRepository public net.minecraft.client.Minecraft func_71386_F()L # getSystemTime public net.minecraft.client.Minecraft func_71383_b(I)V # updateDebugProfilerName @@ -36,6 +37,8 @@ public net.minecraft.entity.Entity field_70180_af # dataWatcher # ResourcePackRepository public net.minecraft.client.resources.ResourcePackRepository field_148534_e # dirServerResourcepacks +public net.minecraft.client.resources.ResourcePackRepository field_177321_h # lock +public net.minecraft.client.resources.ResourcePackRepository field_177322_i # httpRequest # EntityRenderer public net.minecraft.client.renderer.EntityRenderer field_147711_ac # resourceManager