From d30ef19c895fd6db6490a71afdb9f95e38f1d0f1 Mon Sep 17 00:00:00 2001 From: CrushedPixel Date: Tue, 9 Jun 2015 16:08:06 +0200 Subject: [PATCH] Added Event Markers to Replay, which allow players to quickly mark important events while recording (M key by default) Added Event Marker Indicators to be displayed on GuiTimeline --- .../replaymod/events/KeyInputHandler.java | 15 +++++- .../gui/elements/GuiKeyframeTimeline.java | 9 ++-- .../replaymod/gui/elements/GuiTimeline.java | 38 +++++++++++++++ .../replaymod/holders/Marker.java | 45 ++++++++++++++++++ .../recording/ConnectionEventHandler.java | 13 +++++ .../replaymod/recording/DataListener.java | 9 ++-- .../replaymod/recording/PacketListener.java | 15 ++++++ .../replaymod/registry/KeybindRegistry.java | 4 +- .../replaymod/replay/ReplayHandler.java | 45 +++++++++++++++++- .../replaymod/utils/ReplayFile.java | 34 +++++++++++-- .../replaymod/utils/ReplayFileIO.java | 24 ++++++++-- .../assets/replaymod/lang/en_US.lang | 4 ++ .../resources/assets/replaymod/replay_gui.png | Bin 6425 -> 6499 bytes 13 files changed, 235 insertions(+), 20 deletions(-) create mode 100644 src/main/java/eu/crushedpixel/replaymod/holders/Marker.java diff --git a/src/main/java/eu/crushedpixel/replaymod/events/KeyInputHandler.java b/src/main/java/eu/crushedpixel/replaymod/events/KeyInputHandler.java index 58f40b8c..4363e2fd 100755 --- a/src/main/java/eu/crushedpixel/replaymod/events/KeyInputHandler.java +++ b/src/main/java/eu/crushedpixel/replaymod/events/KeyInputHandler.java @@ -4,6 +4,7 @@ import eu.crushedpixel.replaymod.ReplayMod; import eu.crushedpixel.replaymod.entities.CameraEntity.MoveDirection; import eu.crushedpixel.replaymod.gui.GuiKeyframeRepository; import eu.crushedpixel.replaymod.gui.GuiMouseInput; +import eu.crushedpixel.replaymod.recording.ConnectionEventHandler; import eu.crushedpixel.replaymod.registry.KeybindRegistry; import eu.crushedpixel.replaymod.registry.PlayerHandler; import eu.crushedpixel.replaymod.replay.ReplayHandler; @@ -21,6 +22,8 @@ public class KeyInputHandler { private long prevKeysDown = Sys.getTime(); public void onKeyInput() throws Exception { + if(!ReplayHandler.isInReplay()) return; + if(!Keyboard.isCreated()) Keyboard.create(); Keyboard.poll(); @@ -96,8 +99,6 @@ public class KeyInputHandler { @SubscribeEvent public void keyInput(InputEvent.KeyInputEvent event) { - if (!ReplayHandler.isInReplay()) return; - try { onKeyInput(); } catch(Exception e) { @@ -124,6 +125,16 @@ public class KeyInputHandler { public void handleCustomKeybindings(KeyBinding kb, boolean found, int keyCode) { //Custom registered handlers + if(kb.getKeyDescription().equals(KeybindRegistry.KEY_ADD_MARKER) && (kb.isPressed() || kb.getKeyCode() == keyCode)) { + if(ReplayHandler.isInReplay()) { + ReplayHandler.addMarker(); + } else if(ConnectionEventHandler.isRecording()) { + ConnectionEventHandler.addMarker(); + } + } + + if(!ReplayHandler.isInReplay()) return; + if(kb.getKeyDescription().equals(KeybindRegistry.KEY_PLAY_PAUSE) && (kb.isPressed() || kb.getKeyCode() == keyCode) && !ReplayHandler.isInPath()) { ReplayMod.overlay.togglePlayPause(); } diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/elements/GuiKeyframeTimeline.java b/src/main/java/eu/crushedpixel/replaymod/gui/elements/GuiKeyframeTimeline.java index 22873381..1afe9236 100644 --- a/src/main/java/eu/crushedpixel/replaymod/gui/elements/GuiKeyframeTimeline.java +++ b/src/main/java/eu/crushedpixel/replaymod/gui/elements/GuiKeyframeTimeline.java @@ -24,6 +24,7 @@ public class GuiKeyframeTimeline extends GuiTimeline { public GuiKeyframeTimeline(int positionX, int positionY, int width) { super(positionX, positionY, width); showMarkers = true; + showMarkerIndicators = false; } @Override @@ -130,8 +131,8 @@ public class GuiKeyframeTimeline extends GuiTimeline { } if(nextSpectatorKeyframeRealTime != -1) { - int keyframeX = getKeyframeX(kf.getRealTimestamp(), leftTime, rightTime, bodyWidth, segmentLength); - int nextX = getKeyframeX(nextSpectatorKeyframeRealTime, leftTime, rightTime, bodyWidth, segmentLength); + int keyframeX = getKeyframeX(kf.getRealTimestamp(), leftTime, bodyWidth, segmentLength); + int nextX = getKeyframeX(nextSpectatorKeyframeRealTime, leftTime, bodyWidth, segmentLength); drawGradientRect(keyframeX + 2, positionY + BORDER_TOP + 1, nextX - 2, positionY + BORDER_TOP + 4, 0xFF0080FF, 0xFF0080FF); } @@ -154,7 +155,7 @@ public class GuiKeyframeTimeline extends GuiTimeline { } } - private int getKeyframeX(int timestamp, long leftTime, long rightTime, int bodyWidth, double segmentLength) { + private int getKeyframeX(int timestamp, long leftTime, int bodyWidth, double segmentLength) { long positionInSegment = timestamp - leftTime; double fractionOfSegment = positionInSegment / segmentLength; return (int) (positionX + BORDER_LEFT + fractionOfSegment * bodyWidth); @@ -166,7 +167,7 @@ public class GuiKeyframeTimeline extends GuiTimeline { int textureY; int y = positionY; - int keyframeX = getKeyframeX(kf.getRealTimestamp(), leftTime, rightTime, bodyWidth, segmentLength); + int keyframeX = getKeyframeX(kf.getRealTimestamp(), leftTime, bodyWidth, segmentLength); if (kf instanceof PositionKeyframe) { textureX = KEYFRAME_PLACE_X; diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/elements/GuiTimeline.java b/src/main/java/eu/crushedpixel/replaymod/gui/elements/GuiTimeline.java index 05d6b63a..6e24dd4a 100644 --- a/src/main/java/eu/crushedpixel/replaymod/gui/elements/GuiTimeline.java +++ b/src/main/java/eu/crushedpixel/replaymod/gui/elements/GuiTimeline.java @@ -1,6 +1,8 @@ package eu.crushedpixel.replaymod.gui.elements; import eu.crushedpixel.replaymod.ReplayMod; +import eu.crushedpixel.replaymod.holders.Marker; +import eu.crushedpixel.replaymod.replay.ReplayHandler; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.Gui; @@ -25,6 +27,11 @@ public class GuiTimeline extends Gui implements GuiElement { protected static final int BORDER_RIGHT = 4; protected static final int BODY_WIDTH = TEXTURE_WIDTH - BORDER_LEFT - BORDER_RIGHT; + protected static final int MARKER_TEXTURE_X = 40; + protected static final int MARKER_TEXTURE_Y = 39; + protected static final int MARKER_TEXTURE_WIDTH = 5; + protected static final int MARKER_TEXTURE_HEIGHT = 5; + /** * Current position of the cursor. Should normally be between 0 and {@link #timelineLength}. */ @@ -52,6 +59,11 @@ public class GuiTimeline extends Gui implements GuiElement { */ public boolean showMarkers; + /** + * Whether to draw indicators for Replay Markers. + */ + public boolean showMarkerIndicators = true; + protected final int positionX; protected final int positionY; protected final int width; @@ -138,9 +150,35 @@ public class GuiTimeline extends Gui implements GuiElement { } } + if(showMarkerIndicators) { + double segmentLength = timelineLength * zoom; + + for(Marker marker : ReplayHandler.getMarkers()) { + if(marker.getTimestamp() <= rightTime && marker.getTimestamp() >= leftTime) { + int textureX = MARKER_TEXTURE_X; + int textureY = MARKER_TEXTURE_Y; + int y = positionY; + + int markerX = getMarkerX(marker.getTimestamp(), leftTime, bodyWidth, segmentLength); + + if(ReplayHandler.isSelected(marker)) { + textureX += MARKER_TEXTURE_WIDTH; + } + + rect(markerX - 2, y + HEIGHT - 3 - MARKER_TEXTURE_HEIGHT, textureX, textureY, MARKER_TEXTURE_WIDTH, MARKER_TEXTURE_HEIGHT); + } + } + } + drawTimelineCursor(leftTime, rightTime, bodyWidth); } + private int getMarkerX(int timestamp, long leftTime, int bodyWidth, double segmentLength) { + long positionInSegment = timestamp - leftTime; + double fractionOfSegment = positionInSegment / segmentLength; + return (int) (positionX + BORDER_LEFT + fractionOfSegment * bodyWidth); + } + protected void drawTimelineCursor(long leftTime, long rightTime, int bodyWidth) { // Draw the cursor if it's on the current timeline segment if (cursorPosition >= leftTime && cursorPosition <= rightTime) { diff --git a/src/main/java/eu/crushedpixel/replaymod/holders/Marker.java b/src/main/java/eu/crushedpixel/replaymod/holders/Marker.java new file mode 100644 index 00000000..7c86bfed --- /dev/null +++ b/src/main/java/eu/crushedpixel/replaymod/holders/Marker.java @@ -0,0 +1,45 @@ +package eu.crushedpixel.replaymod.holders; + +import org.apache.commons.lang3.builder.HashCodeBuilder; + +public class Marker { + + public Marker(Position position, int timestamp, String name) { + this.position = position; + this.timestamp = timestamp; + this.name = name; + } + + private Position position; + private int timestamp; + private String name; + + public Position getPosition() { + return position; + } + + public int getTimestamp() { + return timestamp; + } + + public String getName() { + return name; + } + + @Override + public boolean equals(Object o2) { + if(o2 == null) return false; + if(!(o2 instanceof Marker)) return false; + Marker m2 = (Marker)o2; + return hashCode() == m2.hashCode(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37) + .append(position) + .append(timestamp) + .append(name) + .toHashCode(); + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/recording/ConnectionEventHandler.java b/src/main/java/eu/crushedpixel/replaymod/recording/ConnectionEventHandler.java index 48c9d702..5ca2f6cf 100755 --- a/src/main/java/eu/crushedpixel/replaymod/recording/ConnectionEventHandler.java +++ b/src/main/java/eu/crushedpixel/replaymod/recording/ConnectionEventHandler.java @@ -59,6 +59,19 @@ public class ConnectionEventHandler { } } + public static void addMarker() { + if(!isRecording || packetListener == null) { + String reason = isRecording ? " (recording)" : " (null)"; + logger.error("Invalid attempt to insert Marker!" + reason); + return; + } + try { + packetListener.addMarker(); + } catch(Exception e) { + e.printStackTrace(); + } + } + @SubscribeEvent public void onConnectedToServerEvent(ClientConnectedToServerEvent event) { ReplayMod.chatMessageHandler.initialize(); diff --git a/src/main/java/eu/crushedpixel/replaymod/recording/DataListener.java b/src/main/java/eu/crushedpixel/replaymod/recording/DataListener.java index c73a40ab..8f98f03a 100755 --- a/src/main/java/eu/crushedpixel/replaymod/recording/DataListener.java +++ b/src/main/java/eu/crushedpixel/replaymod/recording/DataListener.java @@ -4,6 +4,7 @@ import com.google.common.hash.Hashing; import com.google.common.io.Files; import com.google.gson.Gson; import eu.crushedpixel.replaymod.ReplayMod; +import eu.crushedpixel.replaymod.holders.Marker; import eu.crushedpixel.replaymod.holders.PacketData; import eu.crushedpixel.replaymod.utils.ReplayFile; import eu.crushedpixel.replaymod.utils.ReplayFileIO; @@ -34,6 +35,7 @@ public abstract class DataListener extends ChannelInboundHandlerAdapter { protected boolean alive = true; protected DataWriter dataWriter; protected Set players = new HashSet(); + protected Set markers = new HashSet(); private boolean singleplayer; private Gson gson = new Gson(); @@ -43,7 +45,6 @@ public abstract class DataListener extends ChannelInboundHandlerAdapter { 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; @@ -77,7 +78,7 @@ public abstract class DataListener extends ChannelInboundHandlerAdapter { @Override public void channelInactive(ChannelHandlerContext ctx) { - dataWriter.requestFinish(players); + dataWriter.requestFinish(players, markers); } protected void recordResourcePack(File file, int requestId) { @@ -153,7 +154,7 @@ public abstract class DataListener extends ChannelInboundHandlerAdapter { queue.add(data); } - public void requestFinish(Set players) { + public void requestFinish(Set players, Set markers) { active = false; try { @@ -177,7 +178,7 @@ public abstract class DataListener extends ChannelInboundHandlerAdapter { File archive = new File(folder, name + ReplayFile.ZIP_FILE_EXTENSION); archive.createNewFile(); - ReplayFileIO.writeReplayFile(archive, file, metaData, resourcePacks, requestToHash); + ReplayFileIO.writeReplayFile(archive, file, metaData, markers, resourcePacks, requestToHash); file.delete(); FileUtils.deleteDirectory(tempResourcePacksFolder); diff --git a/src/main/java/eu/crushedpixel/replaymod/recording/PacketListener.java b/src/main/java/eu/crushedpixel/replaymod/recording/PacketListener.java index e3aca144..81ad5d81 100755 --- a/src/main/java/eu/crushedpixel/replaymod/recording/PacketListener.java +++ b/src/main/java/eu/crushedpixel/replaymod/recording/PacketListener.java @@ -5,7 +5,11 @@ 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.ReplayMod; +import eu.crushedpixel.replaymod.chat.ChatMessageHandler; +import eu.crushedpixel.replaymod.holders.Marker; import eu.crushedpixel.replaymod.holders.PacketData; +import eu.crushedpixel.replaymod.holders.Position; import eu.crushedpixel.replaymod.utils.ReplayFileIO; import io.netty.channel.ChannelHandlerContext; import net.minecraft.client.Minecraft; @@ -215,6 +219,17 @@ public class PacketListener extends DataListener { return requestId; } + public void addMarker() { + Position pos = new Position(Minecraft.getMinecraft().getRenderViewEntity()); + int timestamp = (int) (System.currentTimeMillis() - startTime); + + Marker marker = new Marker(pos, timestamp, null); + + markers.add(marker); + + ReplayMod.chatMessageHandler.addLocalizedChatMessage("replaymod.chat.addedmarker", ChatMessageHandler.ChatMessageType.INFORMATION); + } + private void downloadResourcePackFuture(int requestId, String url, final String hash) { Futures.addCallback(downloadResourcePack(requestId, url, hash), new FutureCallback() { public void onSuccess(Object result) { diff --git a/src/main/java/eu/crushedpixel/replaymod/registry/KeybindRegistry.java b/src/main/java/eu/crushedpixel/replaymod/registry/KeybindRegistry.java index f087f820..6a03be28 100755 --- a/src/main/java/eu/crushedpixel/replaymod/registry/KeybindRegistry.java +++ b/src/main/java/eu/crushedpixel/replaymod/registry/KeybindRegistry.java @@ -20,17 +20,19 @@ public class KeybindRegistry { public static final String KEY_ROLL_COUNTERCLOCKWISE = "replaymod.input.rollcounterclockwise"; public static final String KEY_RESET_TILT = "replaymod.input.resettilt"; public static final String KEY_PLAY_PAUSE = "replaymod.input.playpause"; + public static final String KEY_ADD_MARKER = "replaymod.input.marker"; private static Minecraft mc = Minecraft.getMinecraft(); public static void initialize() { List bindings = new ArrayList(Arrays.asList(mc.gameSettings.keyBindings)); - bindings.add(new KeyBinding(KEY_LIGHTING, Keyboard.KEY_M, "replaymod.title")); + bindings.add(new KeyBinding(KEY_ADD_MARKER, Keyboard.KEY_M, "replaymod.title")); bindings.add(new KeyBinding(KEY_THUMBNAIL, Keyboard.KEY_N, "replaymod.title")); bindings.add(new KeyBinding(KEY_PLAYER_OVERVIEW, Keyboard.KEY_B, "replaymod.title")); bindings.add(new KeyBinding(KEY_SYNC_TIMELINE, Keyboard.KEY_V, "replaymod.title")); bindings.add(new KeyBinding(KEY_CLEAR_KEYFRAMES, Keyboard.KEY_C, "replaymod.title")); bindings.add(new KeyBinding(KEY_KEYFRAME_PRESETS, Keyboard.KEY_X, "replaymod.title")); + bindings.add(new KeyBinding(KEY_LIGHTING, Keyboard.KEY_Z, "replaymod.title")); bindings.add(new KeyBinding(KEY_ROLL_CLOCKWISE, Keyboard.KEY_L, "replaymod.title")); bindings.add(new KeyBinding(KEY_RESET_TILT, Keyboard.KEY_K, "replaymod.title")); bindings.add(new KeyBinding(KEY_ROLL_COUNTERCLOCKWISE, Keyboard.KEY_J, "replaymod.title")); diff --git a/src/main/java/eu/crushedpixel/replaymod/replay/ReplayHandler.java b/src/main/java/eu/crushedpixel/replaymod/replay/ReplayHandler.java index 5eb84b83..bb4a7c2b 100755 --- a/src/main/java/eu/crushedpixel/replaymod/replay/ReplayHandler.java +++ b/src/main/java/eu/crushedpixel/replaymod/replay/ReplayHandler.java @@ -40,6 +40,7 @@ public class ReplayHandler { private static float cameraTilt = 0; private static KeyframeSet[] keyframeRepository = new KeyframeSet[]{}; + private static Marker[] markers = new Marker[]{}; /** * The file currently being played. @@ -55,7 +56,6 @@ public class ReplayHandler { if(write) { try { File tempFile = File.createTempFile(ReplayFile.ENTRY_PATHS, "json"); - tempFile.deleteOnExit(); ReplayFileIO.writeKeyframeRegistryToFile(repo, tempFile); @@ -66,6 +66,23 @@ public class ReplayHandler { } } + public static Marker[] getMarkers() { return markers; } + + public static void setMarkers(Marker[] m, boolean write) { + markers = m; + if(write) { + try { + File tempFile = File.createTempFile(ReplayFile.ENTRY_MARKERS, "json"); + + ReplayFileIO.writeMarkersToFile(m, tempFile); + + ReplayMod.replayFileAppender.registerModifiedFile(tempFile, ReplayFile.ENTRY_MARKERS, getReplayFile()); + } catch(Exception e) { + e.printStackTrace(); + } + } + } + public static void useKeyframePresetFromRepository(int index) { keyframes = new ArrayList(Arrays.asList(keyframeRepository[index].getKeyframes())); } @@ -145,6 +162,24 @@ public class ReplayHandler { Collections.sort(keyframes, new KeyframeComparator()); } + public static void addMarker() { + Position pos = new Position(mc.getRenderViewEntity()); + int timestamp = ReplayMod.replaySender.currentTimeStamp(); + addMarker(new Marker(pos, timestamp, null)); + } + + public static void addMarker(Marker marker) { + List markerList = new ArrayList(Arrays.asList(markers)); + markerList.add(marker); + markers = markerList.toArray(new Marker[markerList.size()]); + } + + public static void removeMarker(Marker marker) { + List markerList = new ArrayList(Arrays.asList(markers)); + markerList.remove(marker); + markers = markerList.toArray(new Marker[markerList.size()]); + } + public static void addKeyframe(Keyframe keyframe) { keyframes.add(keyframe); selectKeyframe(keyframe); @@ -330,6 +365,11 @@ public class ReplayHandler { return kf == selectedKeyframe; } + public static boolean isSelected(Marker marker) { + //TODO: Make marker selectable + return false; + } + public static void selectKeyframe(Keyframe kf) { selectedKeyframe = kf; sortKeyframes(); @@ -381,6 +421,9 @@ public class ReplayHandler { KeyframeSet[] paths = currentReplayFile.paths().get(); ReplayHandler.setKeyframeRepository(paths == null ? new KeyframeSet[0] : paths, false); + Marker[] markers = currentReplayFile.markers().get(); + ReplayHandler.setMarkers(markers == null ? new Marker[0] : markers, false); + PlayerVisibility visibility = currentReplayFile.visibility().get(); PlayerHandler.loadPlayerVisibilityConfiguration(visibility); diff --git a/src/main/java/eu/crushedpixel/replaymod/utils/ReplayFile.java b/src/main/java/eu/crushedpixel/replaymod/utils/ReplayFile.java index fce7433d..aa64f5ab 100644 --- a/src/main/java/eu/crushedpixel/replaymod/utils/ReplayFile.java +++ b/src/main/java/eu/crushedpixel/replaymod/utils/ReplayFile.java @@ -5,6 +5,7 @@ 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.Marker; import eu.crushedpixel.replaymod.holders.PlayerVisibility; import eu.crushedpixel.replaymod.recording.ReplayMetaData; import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; @@ -30,6 +31,7 @@ public class ReplayFile extends ZipFile { public static final String ENTRY_RESOURCE_PACK_INDEX = "resourcepack/index.json"; public static final String ENTRY_VISIBILITY_OLD = "visibility"; public static final String ENTRY_VISIBILITY = "visibility" + JSON_FILE_EXTENSION; + public static final String ENTRY_MARKERS = "markers" + JSON_FILE_EXTENSION; private final File file; @@ -82,11 +84,6 @@ public class ReplayFile extends ZipFile { return newEntry != null ? newEntry : getEntry(ENTRY_PATHS_OLD); } - public ZipArchiveEntry visibilityEntry() { - ZipArchiveEntry newEntry = getEntry(ENTRY_VISIBILITY); - return newEntry != null ? newEntry : getEntry(ENTRY_VISIBILITY_OLD); - } - public Supplier paths() { return new Supplier() { @Override @@ -105,6 +102,11 @@ public class ReplayFile extends ZipFile { }; } + public ZipArchiveEntry visibilityEntry() { + ZipArchiveEntry newEntry = getEntry(ENTRY_VISIBILITY); + return newEntry != null ? newEntry : getEntry(ENTRY_VISIBILITY_OLD); + } + public Supplier visibility() { return new Supplier() { @Override @@ -123,6 +125,28 @@ public class ReplayFile extends ZipFile { }; } + public ZipArchiveEntry markersEntry() { + return getEntry(ENTRY_MARKERS); + } + + public Supplier markers() { + return new Supplier() { + @Override + public Marker[] get() { + try { + ZipArchiveEntry entry = markersEntry(); + if (entry == null) { + return null; + } + BufferedReader reader = new BufferedReader(new InputStreamReader(getInputStream(entry))); + return new Gson().fromJson(reader, Marker[].class); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + }; + } + public ZipArchiveEntry thumbEntry() { return getEntry(ENTRY_THUMB); } diff --git a/src/main/java/eu/crushedpixel/replaymod/utils/ReplayFileIO.java b/src/main/java/eu/crushedpixel/replaymod/utils/ReplayFileIO.java index aefa4d7c..04d9935e 100755 --- a/src/main/java/eu/crushedpixel/replaymod/utils/ReplayFileIO.java +++ b/src/main/java/eu/crushedpixel/replaymod/utils/ReplayFileIO.java @@ -3,6 +3,7 @@ package eu.crushedpixel.replaymod.utils; import com.google.gson.Gson; import eu.crushedpixel.replaymod.ReplayMod; import eu.crushedpixel.replaymod.holders.KeyframeSet; +import eu.crushedpixel.replaymod.holders.Marker; import eu.crushedpixel.replaymod.holders.PacketData; import eu.crushedpixel.replaymod.holders.PlayerVisibility; import eu.crushedpixel.replaymod.recording.PacketSerializer; @@ -57,7 +58,7 @@ public class ReplayFileIO { return files; } - public static void writeReplayFile(File replayFile, File tempFile, ReplayMetaData metaData, + public static void writeReplayFile(File replayFile, File tempFile, ReplayMetaData metaData, Set markers, Map resourcePacks, Map resourcePackRequests) throws IOException { byte[] buffer = new byte[1024]; @@ -86,14 +87,23 @@ public class ReplayFileIO { fis.close(); zos.closeEntry(); - if (!resourcePackRequests.isEmpty()) { + if(!markers.isEmpty()) { + zos.putNextEntry(new ZipEntry(ReplayFile.ENTRY_MARKERS)); + + pw = new PrintWriter(zos); + pw.write(new Gson().toJson(markers.toArray(new Marker[markers.size()]))); + pw.flush(); + 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()) { + 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(); @@ -322,6 +332,14 @@ public class ReplayFileIO { FileUtils.write(file, json); } + public static void writeMarkersToFile(Marker[] markers, File file) throws IOException { + file.mkdirs(); + file.createNewFile(); + + String json = gson.toJson(markers); + FileUtils.write(file, json); + } + /** * Adds Files as entries to a ZIP File. * @param zipFile The ZIP File to add the files to. diff --git a/src/main/resources/assets/replaymod/lang/en_US.lang b/src/main/resources/assets/replaymod/lang/en_US.lang index b3a047eb..8060d102 100644 --- a/src/main/resources/assets/replaymod/lang/en_US.lang +++ b/src/main/resources/assets/replaymod/lang/en_US.lang @@ -37,6 +37,8 @@ replaymod.chat.savingthumb=Saving Thumbnail... replaymod.chat.savedthumb=Thumbnail has been successfully saved replaymod.chat.failedthumb=Thumbnail could not be saved +replaymod.chat.addedmarker=Event Marker has been added + #Chat messages displayed in Replay Viewer replaymod.chat.notenoughkeyframes=At least 2 position or time keyframes required replaymod.chat.pathstarted=Camera Path started @@ -156,6 +158,7 @@ replaymod.gui.upload.start=Start Upload replaymod.gui.upload.cancel=Cancel Upload replaymod.gui.upload.tagshint=Tags separated by comma replaymod.gui.upload.namehint=Replay Title +replaymod.gui.upload.descriptionhint=Description replaymod.gui.upload.uploading=Uploading... replaymod.gui.upload.success=File has been successfully uploaded replaymod.gui.upload.canceled=Upload has been canceled @@ -226,6 +229,7 @@ replaymod.input.rollclockwise=Roll Clockwise replaymod.input.rollcounterclockwise=Roll Counterclockwise replaymod.input.resettilt=Reset Camera Tilt replaymod.input.playpause=Play/Pause Replay +replaymod.input.marker=Add Event Marker #Keyframe Presets GUI replaymod.gui.keyframerepository.title=Keyframe Repository diff --git a/src/main/resources/assets/replaymod/replay_gui.png b/src/main/resources/assets/replaymod/replay_gui.png index 535beefe1d7630b0af743027b771bf5036c64811..91edbce3c5a1b39804d45182d3f3d6ccfa012a92 100755 GIT binary patch delta 6483 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 ZZf?>RNCj+dk^Yf@G}ZL(eNeFu`5$-)AQAuo literal 6425 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 B0y_Wz