Clean up parts of the recording module and add support for replay recovery after a crash
This commit is contained in:
Submodule ReplayStudio updated: a91987f72e...3895d9535f
2
jGui
2
jGui
Submodule jGui updated: 96bdfcb7d9...611fa2a4ef
@@ -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) {
|
||||
|
||||
86
src/main/java/com/replaymod/core/gui/RestoreReplayGui.java
Normal file
86
src/main/java/com/replaymod/core/gui/RestoreReplayGui.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -376,7 +376,7 @@ public class RecordingEventHandler {
|
||||
if (mc.isIntegratedServerRunning()) {
|
||||
IntegratedServer server = mc.getIntegratedServer();
|
||||
if (server != null && server.isGamePaused) {
|
||||
packetListener.serverWasPaused = true;
|
||||
packetListener.setServerWasPaused();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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,10 +145,7 @@ public class PacketListener extends DataListener {
|
||||
}
|
||||
}
|
||||
this.context = ctx;
|
||||
if(!alive) {
|
||||
super.channelRead(ctx, msg);
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
Integer packetId = EnumConnectionState.PLAY.getPacketId(EnumPacketDirection.CLIENTBOUND, packet);
|
||||
if (packetId == null) {
|
||||
throw new IOException("Unknown packet type:" + packet.getClass());
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
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();
|
||||
public void addMarker() {
|
||||
AdvancedPosition pos = new AdvancedPosition(Minecraft.getMinecraft().getRenderViewEntity());
|
||||
int timestamp = (int) (System.currentTimeMillis() - startTime);
|
||||
|
||||
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));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return 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();
|
||||
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 {
|
||||
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);
|
||||
Set<Marker> markers = replayFile.getMarkers().or(HashSet::new);
|
||||
markers.add(marker);
|
||||
replayFile.writeMarkers(markers);
|
||||
} catch (IOException e) {
|
||||
logger.error("Writing markers:", e);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user