From b2ad52d5b68f6e1908afb717e716589b631f5789 Mon Sep 17 00:00:00 2001 From: CrushedPixel Date: Tue, 9 Jun 2015 22:55:20 +0200 Subject: [PATCH] Markers are now subclasses of Keyframe Markers can now be moved and jumped to on the Replay Timeline --- .../replaymod/entities/CameraEntity.java | 6 ++ .../gui/elements/GuiKeyframeTimeline.java | 39 ++++++++--- .../replaymod/gui/elements/GuiTimeline.java | 32 --------- .../gui/overlay/GuiReplayOverlay.java | 31 ++++++++- .../{Marker.java => MarkerKeyframe.java} | 29 +++++---- .../recording/ConnectionEventHandler.java | 2 +- .../replaymod/recording/DataListener.java | 6 +- .../replaymod/recording/PacketListener.java | 4 +- .../replaymod/replay/ReplayHandler.java | 61 +++++++++++------- .../replaymod/replay/ReplaySender.java | 4 +- .../replaymod/utils/ReplayFile.java | 10 +-- .../replaymod/utils/ReplayFileIO.java | 8 +-- .../resources/assets/replaymod/replay_gui.png | Bin 6499 -> 6694 bytes 13 files changed, 133 insertions(+), 99 deletions(-) rename src/main/java/eu/crushedpixel/replaymod/holders/{Marker.java => MarkerKeyframe.java} (61%) diff --git a/src/main/java/eu/crushedpixel/replaymod/entities/CameraEntity.java b/src/main/java/eu/crushedpixel/replaymod/entities/CameraEntity.java index cdffaf32..3534cf19 100755 --- a/src/main/java/eu/crushedpixel/replaymod/entities/CameraEntity.java +++ b/src/main/java/eu/crushedpixel/replaymod/entities/CameraEntity.java @@ -152,6 +152,12 @@ public class CameraEntity extends EntityPlayer { direction = direction.normalize().add(new Vec3(oldDir.xCoord * (motion / 4f), oldDir.yCoord * (motion / 4f), oldDir.zCoord * (motion / 4f)).normalize()); } + public void moveAbsolute(Position pos) { + this.moveAbsolute(pos.getX(), pos.getY(), pos.getZ()); + rotationPitch = pos.getPitch(); + rotationYaw = pos.getYaw(); + } + public void moveAbsolute(double x, double y, double z) { if(ReplayHandler.isInPath()) return; this.lastTickPosX = this.prevPosX = this.posX = x; 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 1afe9236..e4211e7b 100644 --- a/src/main/java/eu/crushedpixel/replaymod/gui/elements/GuiKeyframeTimeline.java +++ b/src/main/java/eu/crushedpixel/replaymod/gui/elements/GuiKeyframeTimeline.java @@ -1,7 +1,9 @@ package eu.crushedpixel.replaymod.gui.elements; +import eu.crushedpixel.replaymod.ReplayMod; import eu.crushedpixel.replaymod.gui.GuiEditKeyframe; import eu.crushedpixel.replaymod.holders.Keyframe; +import eu.crushedpixel.replaymod.holders.MarkerKeyframe; import eu.crushedpixel.replaymod.holders.PositionKeyframe; import eu.crushedpixel.replaymod.holders.TimeKeyframe; import eu.crushedpixel.replaymod.replay.ReplayHandler; @@ -16,15 +18,23 @@ public class GuiKeyframeTimeline extends GuiTimeline { private static final int KEYFRAME_TIME_Y = 25; private static final int KEYFRAME_SPEC_X = 74; private static final int KEYFRAME_SPEC_Y = 30; + private static final int KEYFRAME_MARKER_X = 40; + private static final int KEYFRAME_MARKER_Y = 39; private Keyframe clickedKeyFrame; private long clickTime; private boolean dragging; + private boolean markerKeyframes; + private boolean timeKeyframes; + private boolean placeKeyframes; - public GuiKeyframeTimeline(int positionX, int positionY, int width) { + public GuiKeyframeTimeline(int positionX, int positionY, int width, boolean showMarkers, + boolean showMarkerKeyframes, boolean showPlaceKeyframes, boolean showTimeKeyframes) { super(positionX, positionY, width); - showMarkers = true; - showMarkerIndicators = false; + this.showMarkers = showMarkers; + this.markerKeyframes = showMarkerKeyframes; + this.timeKeyframes = showTimeKeyframes; + this.placeKeyframes = showPlaceKeyframes; } @Override @@ -34,13 +44,15 @@ public class GuiKeyframeTimeline extends GuiTimeline { return; } - int tolerance = (int) (2 * Math.round(zoom * timelineLength / width)); + int tolerance = (int)(2 * Math.round(zoom * timelineLength / width)); Keyframe closest; - if (mouseY >= positionY + BORDER_TOP + 5) { + if (mouseY >= positionY + BORDER_TOP + 5 && timeKeyframes) { closest = ReplayHandler.getClosestTimeKeyframeForRealTime((int) time, tolerance); - } else if (mouseY >= positionY + BORDER_TOP) { + } else if (mouseY >= positionY + BORDER_TOP && placeKeyframes) { closest = ReplayHandler.getClosestPlaceKeyframeForRealTime((int) time, tolerance); + } else if (mouseY >= positionY + BORDER_TOP + 10 && markerKeyframes) { + closest = ReplayHandler.getClosestMarkerForRealTime((int) time, tolerance); } else { closest = null; } @@ -85,6 +97,10 @@ public class GuiKeyframeTimeline extends GuiTimeline { @Override public void mouseRelease(Minecraft mc, int mouseX, int mouseY, int button) { mouseDrag(mc, mouseX, mouseY, button); + if(clickedKeyFrame instanceof MarkerKeyframe && dragging == false) { + ReplayMod.replaySender.jumpToTime(clickedKeyFrame.getRealTimestamp()); + ReplayHandler.setLastPosition(((MarkerKeyframe)clickedKeyFrame).getPosition()); + } clickedKeyFrame = null; dragging = false; } @@ -169,7 +185,8 @@ public class GuiKeyframeTimeline extends GuiTimeline { int keyframeX = getKeyframeX(kf.getRealTimestamp(), leftTime, bodyWidth, segmentLength); - if (kf instanceof PositionKeyframe) { + if(kf instanceof PositionKeyframe) { + if(!placeKeyframes) return; textureX = KEYFRAME_PLACE_X; textureY = KEYFRAME_PLACE_Y; y += 0; @@ -179,10 +196,16 @@ public class GuiKeyframeTimeline extends GuiTimeline { textureX = KEYFRAME_SPEC_X; textureY = KEYFRAME_SPEC_Y; } - } else if (kf instanceof TimeKeyframe) { + } else if(kf instanceof TimeKeyframe) { + if(!timeKeyframes) return; textureX = KEYFRAME_TIME_X; textureY = KEYFRAME_TIME_Y; y += 5; + } else if(kf instanceof MarkerKeyframe) { + if(!markerKeyframes) return; + textureX = KEYFRAME_MARKER_X; + textureY = KEYFRAME_MARKER_Y; + y += 10; } else { throw new UnsupportedOperationException("Unknown keyframe type: " + kf.getClass()); } 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 6e24dd4a..ac4253f6 100644 --- a/src/main/java/eu/crushedpixel/replaymod/gui/elements/GuiTimeline.java +++ b/src/main/java/eu/crushedpixel/replaymod/gui/elements/GuiTimeline.java @@ -1,8 +1,6 @@ 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; @@ -27,11 +25,6 @@ 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}. */ @@ -59,11 +52,6 @@ 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; @@ -150,26 +138,6 @@ 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); } diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/overlay/GuiReplayOverlay.java b/src/main/java/eu/crushedpixel/replaymod/gui/overlay/GuiReplayOverlay.java index 8d40bbef..270c49c9 100755 --- a/src/main/java/eu/crushedpixel/replaymod/gui/overlay/GuiReplayOverlay.java +++ b/src/main/java/eu/crushedpixel/replaymod/gui/overlay/GuiReplayOverlay.java @@ -7,6 +7,7 @@ import eu.crushedpixel.replaymod.gui.GuiMouseInput; import eu.crushedpixel.replaymod.gui.GuiRenderSettings; import eu.crushedpixel.replaymod.gui.GuiReplaySpeedSlider; import eu.crushedpixel.replaymod.gui.elements.*; +import eu.crushedpixel.replaymod.holders.MarkerKeyframe; import eu.crushedpixel.replaymod.holders.Position; import eu.crushedpixel.replaymod.holders.PositionKeyframe; import eu.crushedpixel.replaymod.holders.TimeKeyframe; @@ -181,6 +182,28 @@ public class GuiReplayOverlay extends Gui { } }; + private final GuiElement buttonMarker = new DelegatingElement() { + + private final GuiElement buttonNotSelected = texturedButton(BUTTON_TIME_X, BOTTOM_ROW, 0, 80, 20, new Runnable() { + @Override + public void run() { + ReplayHandler.addKeyframe(new TimeKeyframe(ReplayHandler.getRealTimelineCursor(), ReplayMod.replaySender.currentTimeStamp())); + } + }, "replaymod.gui.ingame.menu.addtimekeyframe"); + + private final GuiElement buttonSelected = texturedButton(BUTTON_TIME_X, BOTTOM_ROW, 0, 100, 20, new Runnable() { + @Override + public void run() { + ReplayHandler.removeKeyframe(ReplayHandler.getSelectedKeyframe()); + } + }, "replaymod.gui.ingame.menu.removetimekeyframe"); + + @Override + public GuiElement delegate() { + return ReplayHandler.getSelectedKeyframe() instanceof TimeKeyframe ? buttonSelected : buttonNotSelected; + } + }; + private final GuiElement buttonZoomIn = texturedButton(WIDTH - 14 - 9, BOTTOM_ROW, 40, 20, 9, new Runnable() { @Override public void run() { @@ -197,14 +220,16 @@ public class GuiReplayOverlay extends Gui { } }, "replaymod.gui.ingame.menu.zoomout"); - private final GuiTimeline timeline = new GuiTimeline(TIMELINE_X, TOP_ROW - 1, WIDTH - 14 - TIMELINE_X) { + private final GuiKeyframeTimeline timeline = new GuiKeyframeTimeline(TIMELINE_X, TOP_ROW - 1, WIDTH - 14 - TIMELINE_X, false, true, false, false) { @Override public void mouseClick(Minecraft mc, int mouseX, int mouseY, int button) { - performJump(timeline.getTimeAt(mouseX, mouseY)); + super.mouseClick(mc, mouseX, mouseY, button); + if(!(ReplayHandler.getSelectedKeyframe() instanceof MarkerKeyframe)) + performJump(timeline.getTimeAt(mouseX, mouseY)); } }; - private final GuiKeyframeTimeline timelineReal = new GuiKeyframeTimeline(TIMELINE_REAL_X, BOTTOM_ROW - 1, TIMELINE_REAL_WIDTH); + private final GuiKeyframeTimeline timelineReal = new GuiKeyframeTimeline(TIMELINE_REAL_X, BOTTOM_ROW - 1, TIMELINE_REAL_WIDTH, true, false, true, true); { timelineReal.timelineLength = 10 * 60 * 1000; } diff --git a/src/main/java/eu/crushedpixel/replaymod/holders/Marker.java b/src/main/java/eu/crushedpixel/replaymod/holders/MarkerKeyframe.java similarity index 61% rename from src/main/java/eu/crushedpixel/replaymod/holders/Marker.java rename to src/main/java/eu/crushedpixel/replaymod/holders/MarkerKeyframe.java index 7c86bfed..c1351567 100644 --- a/src/main/java/eu/crushedpixel/replaymod/holders/Marker.java +++ b/src/main/java/eu/crushedpixel/replaymod/holders/MarkerKeyframe.java @@ -2,25 +2,26 @@ 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; - } +public class MarkerKeyframe extends Keyframe { private Position position; - private int timestamp; private String name; + @Override + public Object clone() { + return new MarkerKeyframe(this.getPosition(), this.getRealTimestamp(), this.getName()); + } + + public MarkerKeyframe(Position position, int timestamp, String name) { + super(timestamp); + this.position = position; + this.name = name; + } + public Position getPosition() { return position; } - public int getTimestamp() { - return timestamp; - } public String getName() { return name; @@ -29,8 +30,8 @@ public class Marker { @Override public boolean equals(Object o2) { if(o2 == null) return false; - if(!(o2 instanceof Marker)) return false; - Marker m2 = (Marker)o2; + if(!(o2 instanceof MarkerKeyframe)) return false; + MarkerKeyframe m2 = (MarkerKeyframe)o2; return hashCode() == m2.hashCode(); } @@ -38,7 +39,7 @@ public class Marker { public int hashCode() { return new HashCodeBuilder(17, 37) .append(position) - .append(timestamp) + .append(getRealTimestamp()) .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 5ca2f6cf..c7b891aa 100755 --- a/src/main/java/eu/crushedpixel/replaymod/recording/ConnectionEventHandler.java +++ b/src/main/java/eu/crushedpixel/replaymod/recording/ConnectionEventHandler.java @@ -62,7 +62,7 @@ public class ConnectionEventHandler { public static void addMarker() { if(!isRecording || packetListener == null) { String reason = isRecording ? " (recording)" : " (null)"; - logger.error("Invalid attempt to insert Marker!" + reason); + logger.error("Invalid attempt to insert MarkerKeyframe!" + reason); return; } try { diff --git a/src/main/java/eu/crushedpixel/replaymod/recording/DataListener.java b/src/main/java/eu/crushedpixel/replaymod/recording/DataListener.java index 8f98f03a..9656c825 100755 --- a/src/main/java/eu/crushedpixel/replaymod/recording/DataListener.java +++ b/src/main/java/eu/crushedpixel/replaymod/recording/DataListener.java @@ -4,7 +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.MarkerKeyframe; import eu.crushedpixel.replaymod.holders.PacketData; import eu.crushedpixel.replaymod.utils.ReplayFile; import eu.crushedpixel.replaymod.utils.ReplayFileIO; @@ -35,7 +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(); + protected Set markers = new HashSet(); private boolean singleplayer; private Gson gson = new Gson(); @@ -154,7 +154,7 @@ public abstract class DataListener extends ChannelInboundHandlerAdapter { queue.add(data); } - public void requestFinish(Set players, Set markers) { + public void requestFinish(Set players, Set markers) { active = false; try { diff --git a/src/main/java/eu/crushedpixel/replaymod/recording/PacketListener.java b/src/main/java/eu/crushedpixel/replaymod/recording/PacketListener.java index 81ad5d81..e4968ba1 100755 --- a/src/main/java/eu/crushedpixel/replaymod/recording/PacketListener.java +++ b/src/main/java/eu/crushedpixel/replaymod/recording/PacketListener.java @@ -7,7 +7,7 @@ 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.MarkerKeyframe; import eu.crushedpixel.replaymod.holders.PacketData; import eu.crushedpixel.replaymod.holders.Position; import eu.crushedpixel.replaymod.utils.ReplayFileIO; @@ -223,7 +223,7 @@ public class PacketListener extends DataListener { Position pos = new Position(Minecraft.getMinecraft().getRenderViewEntity()); int timestamp = (int) (System.currentTimeMillis() - startTime); - Marker marker = new Marker(pos, timestamp, null); + MarkerKeyframe marker = new MarkerKeyframe(pos, timestamp, null); markers.add(marker); diff --git a/src/main/java/eu/crushedpixel/replaymod/replay/ReplayHandler.java b/src/main/java/eu/crushedpixel/replaymod/replay/ReplayHandler.java index bb4a7c2b..13d45e71 100755 --- a/src/main/java/eu/crushedpixel/replaymod/replay/ReplayHandler.java +++ b/src/main/java/eu/crushedpixel/replaymod/replay/ReplayHandler.java @@ -40,7 +40,6 @@ 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. @@ -66,10 +65,22 @@ public class ReplayHandler { } } - public static Marker[] getMarkers() { return markers; } + public static MarkerKeyframe[] getMarkers() { + List markers = new ArrayList(); + for(Keyframe kf : keyframes) { + if(kf instanceof MarkerKeyframe) markers.add((MarkerKeyframe)kf); + } + return markers.toArray(new MarkerKeyframe[markers.size()]); + } + + public static void setMarkers(MarkerKeyframe[] m, boolean write) { + for(Keyframe kf : new ArrayList(keyframes)) { + if(kf instanceof MarkerKeyframe) keyframes.remove(kf); + } + for(MarkerKeyframe marker : m) { + keyframes.add(marker); + } - public static void setMarkers(Marker[] m, boolean write) { - markers = m; if(write) { try { File tempFile = File.createTempFile(ReplayFile.ENTRY_MARKERS, "json"); @@ -165,19 +176,7 @@ public class ReplayHandler { 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()]); + addKeyframe(new MarkerKeyframe(pos, timestamp, null)); } public static void addKeyframe(Keyframe keyframe) { @@ -287,6 +286,25 @@ public class ReplayHandler { return closest; } + public static MarkerKeyframe getClosestMarkerForRealTime(int realTime, int tolerance) { + List found = new ArrayList(); + for(Keyframe kf : keyframes) { + if(!(kf instanceof MarkerKeyframe)) continue; + if(Math.abs(kf.getRealTimestamp() - realTime) <= tolerance) { + found.add((MarkerKeyframe) kf); + } + } + + MarkerKeyframe closest = null; + + for(MarkerKeyframe kf : found) { + if(closest == null || Math.abs(closest.getRealTimestamp() - realTime) > Math.abs(kf.getRealTimestamp() - realTime)) { + closest = kf; + } + } + return closest; + } + public static PositionKeyframe getPreviousPositionKeyframe(int realTime) { if(keyframes.isEmpty()) return null; PositionKeyframe backup = null; @@ -365,11 +383,6 @@ 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(); @@ -421,8 +434,8 @@ 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); + MarkerKeyframe[] markers = currentReplayFile.markers().get(); + ReplayHandler.setMarkers(markers == null ? new MarkerKeyframe[0] : markers, false); PlayerVisibility visibility = currentReplayFile.visibility().get(); PlayerHandler.loadPlayerVisibilityConfiguration(visibility); diff --git a/src/main/java/eu/crushedpixel/replaymod/replay/ReplaySender.java b/src/main/java/eu/crushedpixel/replaymod/replay/ReplaySender.java index 34bbd388..8aedbb20 100755 --- a/src/main/java/eu/crushedpixel/replaymod/replay/ReplaySender.java +++ b/src/main/java/eu/crushedpixel/replaymod/replay/ReplaySender.java @@ -536,9 +536,7 @@ public class ReplaySender extends ChannelInboundHandlerAdapter { if (cam != null && pos != null) { // Move camera back in case we have been respawned if (Math.abs(pos.getX() - cam.posX) < ReplayMod.TP_DISTANCE_LIMIT && Math.abs(pos.getZ() - cam.posZ) < ReplayMod.TP_DISTANCE_LIMIT) { - cam.moveAbsolute(pos.getX(), pos.getY(), pos.getZ()); - cam.rotationPitch = pos.getPitch(); - cam.rotationYaw = pos.getYaw(); + cam.moveAbsolute(pos); } } diff --git a/src/main/java/eu/crushedpixel/replaymod/utils/ReplayFile.java b/src/main/java/eu/crushedpixel/replaymod/utils/ReplayFile.java index aa64f5ab..7236a860 100644 --- a/src/main/java/eu/crushedpixel/replaymod/utils/ReplayFile.java +++ b/src/main/java/eu/crushedpixel/replaymod/utils/ReplayFile.java @@ -5,7 +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.MarkerKeyframe; import eu.crushedpixel.replaymod.holders.PlayerVisibility; import eu.crushedpixel.replaymod.recording.ReplayMetaData; import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; @@ -129,17 +129,17 @@ public class ReplayFile extends ZipFile { return getEntry(ENTRY_MARKERS); } - public Supplier markers() { - return new Supplier() { + public Supplier markers() { + return new Supplier() { @Override - public Marker[] get() { + public MarkerKeyframe[] 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); + return new Gson().fromJson(reader, MarkerKeyframe[].class); } catch (IOException e) { throw new RuntimeException(e); } diff --git a/src/main/java/eu/crushedpixel/replaymod/utils/ReplayFileIO.java b/src/main/java/eu/crushedpixel/replaymod/utils/ReplayFileIO.java index 04d9935e..ecfb3f6d 100755 --- a/src/main/java/eu/crushedpixel/replaymod/utils/ReplayFileIO.java +++ b/src/main/java/eu/crushedpixel/replaymod/utils/ReplayFileIO.java @@ -3,7 +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.MarkerKeyframe; import eu.crushedpixel.replaymod.holders.PacketData; import eu.crushedpixel.replaymod.holders.PlayerVisibility; import eu.crushedpixel.replaymod.recording.PacketSerializer; @@ -58,7 +58,7 @@ public class ReplayFileIO { return files; } - public static void writeReplayFile(File replayFile, File tempFile, ReplayMetaData metaData, Set markers, + public static void writeReplayFile(File replayFile, File tempFile, ReplayMetaData metaData, Set markers, Map resourcePacks, Map resourcePackRequests) throws IOException { byte[] buffer = new byte[1024]; @@ -91,7 +91,7 @@ public class ReplayFileIO { zos.putNextEntry(new ZipEntry(ReplayFile.ENTRY_MARKERS)); pw = new PrintWriter(zos); - pw.write(new Gson().toJson(markers.toArray(new Marker[markers.size()]))); + pw.write(new Gson().toJson(markers.toArray(new MarkerKeyframe[markers.size()]))); pw.flush(); zos.closeEntry(); } @@ -332,7 +332,7 @@ public class ReplayFileIO { FileUtils.write(file, json); } - public static void writeMarkersToFile(Marker[] markers, File file) throws IOException { + public static void writeMarkersToFile(MarkerKeyframe[] markers, File file) throws IOException { file.mkdirs(); file.createNewFile(); diff --git a/src/main/resources/assets/replaymod/replay_gui.png b/src/main/resources/assets/replaymod/replay_gui.png index 91edbce3c5a1b39804d45182d3f3d6ccfa012a92..864fbc85b4ea0dd133d065037be56e8970b766c1 100755 GIT binary patch delta 6682 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

`+ zsB{A);A+KtZFP*-n;!vD7rRr)wm@TL*UHCYu8#cTew`}O=tL+pcQ!!a?{??_#5XY4 zV(-g)v$d1+CsX%1ds9%Qw0zW2KZ>3^Wa||ct42z=V%m@L521s+X%imLX=L~YkrXTm z=={ZE9|77)S`=BQXRRXnD{7a^ zY#BZU)y~<7n}TPDPV0>S81ToVScjr_-ywWf_&=59E0e!uau;ezxX z`@f&3|1Al0NhSEV+8DfAo;pkID!AN@@zr<*yh!t(T+_V64&I3Je;DHcPH~3}CnV1n z7(X*d-BObbYKf;56BIS&jJ7zxp>~BDjzZ*jsu_M-`x_YYgVi@`c=<9&Gt15GrFe!+ zF@3`PcN2yUZgGjv74EELw*F=-7GIz-#t-wRV)->IWc6lm^8N%4t|x4&&~2ZB`B`lI za{xK&S<-0_z#RU#=G)ov$4g>FuRMR5qt5^Gcz-@qNsQcPhcX>K`h9bq^tH9(GzpE_Qd_+<*%I~M~C`*y7CXbpKg~H zWEeM-#{(!l1U%^tpYh0?HgZoO=&3~ zglj&$zB`dn{vFF*1)3Z^Q8!I*T5-&G>o_eKcxh49GCSPs($)W@<^^isdV9$!iA)v> z_f+zjB_PTp{ZLcE&u%99<5f*R6Iib4Z5B>?y1IVh(V_#$+}4cQ`Y;RUxvbm2F6Hd} zMWboTHIxwot?J*B@o;yyfApvqQ1e$(>irJC_zrB+;=SX7eunfvpbT)(49e(J`KVdw z+4S7*{Ljml$nk*zTB@kdA!74q!B5s+X3|?11oWwgpT5IWZtfT#g{t+SqY#=4hRZ|O WHx6lDtNi;3q@_lsr&^7)kNh71X13h` delta 6485 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