Clean up parts of the recording module and add support for replay recovery after a crash

This commit is contained in:
johni0702
2016-07-29 14:55:22 +02:00
parent eedaf75e9e
commit 68c5d8583a
12 changed files with 484 additions and 424 deletions

View File

@@ -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) {

View File

@@ -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<RestoreReplayGui> {
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<RestoreReplayGui>() {
@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;
}
}

View File

@@ -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);

View File

@@ -376,7 +376,7 @@ public class RecordingEventHandler {
if (mc.isIntegratedServerRunning()) {
IntegratedServer server = mc.getIntegratedServer();
if (server != null && server.isGamePaused) {
packetListener.serverWasPaused = true;
packetListener.setServerWasPaused();
}
}
}

View File

@@ -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<String> players = new HashSet<>();
private boolean singleplayer;
private final Set<Marker> markers = new HashSet<>();
private final Map<Integer, String> 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
}
}
}
}

View File

@@ -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<String> 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<Marker> 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;
}
}

View File

@@ -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();
}
}
}
}

View File

@@ -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<Integer, String> 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();
}
}
}

View File

@@ -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 {

View File

@@ -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