diff --git a/ReplayStudio b/ReplayStudio index 99955dd5..6509ab21 160000 --- a/ReplayStudio +++ b/ReplayStudio @@ -1 +1 @@ -Subproject commit 99955dd5646cb26bf754bee94b5efc5d80919935 +Subproject commit 6509ab21065bfe9f532d04f9f6bae47924a8227d diff --git a/jGui b/jGui index 46e47874..7849fb3b 160000 --- a/jGui +++ b/jGui @@ -1 +1 @@ -Subproject commit 46e47874a46ab4cca5156297c391fde0b25dedca +Subproject commit 7849fb3bb7983c029b0da1962446f9a2d75eac19 diff --git a/src/main/java/com/replaymod/core/ReplayMod.java b/src/main/java/com/replaymod/core/ReplayMod.java index 41e13c8c..de95d13e 100755 --- a/src/main/java/com/replaymod/core/ReplayMod.java +++ b/src/main/java/com/replaymod/core/ReplayMod.java @@ -34,6 +34,7 @@ import java.util.List; @Mod(modid = ReplayMod.MOD_ID, useMetadata = true, + updateJSON = "https://raw.githubusercontent.com/ReplayMod/ReplayMod/master/versions.json", guiFactory = "com.replaymod.core.gui.GuiFactory") public class ReplayMod { diff --git a/src/main/java/com/replaymod/extras/ReplayModExtras.java b/src/main/java/com/replaymod/extras/ReplayModExtras.java index 54a7eca4..750111a6 100644 --- a/src/main/java/com/replaymod/extras/ReplayModExtras.java +++ b/src/main/java/com/replaymod/extras/ReplayModExtras.java @@ -29,8 +29,7 @@ public class ReplayModExtras { FullBrightness.class, HotkeyButtons.class, LocalizationExtra.class, - OpenEyeExtra.class, - VersionChecker.class + OpenEyeExtra.class ); private Logger logger; diff --git a/src/main/java/com/replaymod/extras/VersionChecker.java b/src/main/java/com/replaymod/extras/VersionChecker.java deleted file mode 100644 index 8c0fd12f..00000000 --- a/src/main/java/com/replaymod/extras/VersionChecker.java +++ /dev/null @@ -1,66 +0,0 @@ -package com.replaymod.extras; - -import com.replaymod.core.ReplayMod; -import com.replaymod.online.ReplayModOnline; -import net.minecraft.client.gui.Gui; -import net.minecraft.client.gui.GuiMainMenu; -import net.minecraft.client.resources.I18n; -import net.minecraftforge.client.event.GuiScreenEvent; -import net.minecraftforge.common.MinecraftForge; -import net.minecraftforge.fml.common.Mod; -import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; - -import java.awt.*; -import java.util.List; - - -public class VersionChecker implements Extra { - @Mod.Instance(ReplayModOnline.MOD_ID) - private static ReplayModOnline module; - - @Override - public void register(ReplayMod mod) throws Exception { - final String currentVersion = mod.getVersion(); - new Thread(new Runnable() { - @Override - public void run() { - try { - boolean upToDate = module.getApiClient().isVersionUpToDate(currentVersion); - if (!upToDate) { - MinecraftForge.EVENT_BUS.register(VersionChecker.this); - } - } catch(Exception e) { - e.printStackTrace(); - } - } - }, "ReplayMod-VersionChecker").start(); - } - - @SubscribeEvent - public void onDrawScreen(GuiScreenEvent.DrawScreenEvent.Post event) { - if (!(event.getGui() instanceof GuiMainMenu)) { - return; - } - - int width = Math.max(100, event.getGui().width / 2 - 100 - 10); - - @SuppressWarnings("unchecked") List lines = - event.getGui().mc.fontRendererObj.listFormattedStringToWidth(I18n.format("replaymod.gui.outdated"), width); - - int maxLineWidth = 0; - for(String line : lines) { - int lineWidth = event.getGui().mc.fontRendererObj.getStringWidth(line); - if(lineWidth > maxLineWidth) { - maxLineWidth = lineWidth; - } - } - - Gui.drawRect(2, 77, 5 + maxLineWidth + 3, 80 + (lines.size() * 10), 0x80FF0000); - - int i = 0; - for(String line : lines) { - event.getGui().mc.fontRendererObj.drawStringWithShadow(line, 5, 80 + (i * 10), Color.WHITE.getRGB()); - i++; - } - } -} diff --git a/src/main/java/com/replaymod/online/gui/GuiReplayCenterSearch.java b/src/main/java/com/replaymod/online/gui/GuiReplayCenterSearch.java index 7ecfa148..9a39e54d 100644 --- a/src/main/java/com/replaymod/online/gui/GuiReplayCenterSearch.java +++ b/src/main/java/com/replaymod/online/gui/GuiReplayCenterSearch.java @@ -11,15 +11,18 @@ import de.johni0702.minecraft.gui.element.GuiLabel; import de.johni0702.minecraft.gui.element.GuiTextField; import de.johni0702.minecraft.gui.element.GuiToggleButton; import de.johni0702.minecraft.gui.element.advanced.GuiDropdownMenu; +import de.johni0702.minecraft.gui.function.Typeable; import de.johni0702.minecraft.gui.layout.GridLayout; import de.johni0702.minecraft.gui.popup.AbstractGuiPopup; import de.johni0702.minecraft.gui.utils.Colors; import net.minecraft.client.resources.I18n; +import org.lwjgl.input.Keyboard; +import org.lwjgl.util.ReadablePoint; import java.util.ArrayList; import java.util.List; -public class GuiReplayCenterSearch extends AbstractGuiPopup { +public class GuiReplayCenterSearch extends AbstractGuiPopup implements Typeable { private final GuiReplayCenter replayCenter; private final ApiClient apiClient; public final GuiLabel title = new GuiLabel().setI18nText("replaymod.gui.center.search.filters").setColor(Colors.BLACK); @@ -100,4 +103,13 @@ public class GuiReplayCenterSearch extends AbstractGuiPopup future; @@ -33,6 +34,11 @@ public abstract class AbstractTimelinePlayer { this.replayHandler = replayHandler; } + public ListenableFuture start(Timeline timeline, long from) { + startOffset = from; + return start(timeline); + } + public ListenableFuture start(Timeline timeline) { this.timeline = timeline; diff --git a/src/main/java/com/replaymod/pathing/player/RealtimeTimelinePlayer.java b/src/main/java/com/replaymod/pathing/player/RealtimeTimelinePlayer.java index eca409f4..4271ddbf 100644 --- a/src/main/java/com/replaymod/pathing/player/RealtimeTimelinePlayer.java +++ b/src/main/java/com/replaymod/pathing/player/RealtimeTimelinePlayer.java @@ -46,6 +46,6 @@ public class RealtimeTimelinePlayer extends AbstractTimelinePlayer { @Override public long getTimePassed() { - return firstFrame ? 0 : System.currentTimeMillis() - startTime; + return startOffset + (firstFrame ? 0 : System.currentTimeMillis() - startTime); } } diff --git a/src/main/java/com/replaymod/render/VideoWriter.java b/src/main/java/com/replaymod/render/VideoWriter.java index 6fcce914..66665768 100755 --- a/src/main/java/com/replaymod/render/VideoWriter.java +++ b/src/main/java/com/replaymod/render/VideoWriter.java @@ -8,6 +8,7 @@ import net.minecraft.client.Minecraft; import net.minecraft.crash.CrashReport; import net.minecraft.crash.CrashReportCategory; import org.apache.commons.exec.CommandLine; +import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.io.output.TeeOutputStream; import org.lwjgl.util.ReadableDimension; @@ -34,6 +35,7 @@ public class VideoWriter implements FrameConsumer { this.settings = settings; File outputFolder = settings.getOutputFile().getParentFile(); + FileUtils.forceMkdir(outputFolder); String fileName = settings.getOutputFile().getName(); commandArgs = settings.getExportArguments() diff --git a/src/main/java/com/replaymod/render/gui/GuiRenderSettings.java b/src/main/java/com/replaymod/render/gui/GuiRenderSettings.java index 3e3ddbdb..a9be9391 100644 --- a/src/main/java/com/replaymod/render/gui/GuiRenderSettings.java +++ b/src/main/java/com/replaymod/render/gui/GuiRenderSettings.java @@ -91,7 +91,7 @@ public class GuiRenderSettings extends GuiScreen implements Closeable { }).setSize(122, 20).setSteps(110); public final GuiPanel videoResolutionPanel = new GuiPanel() .setLayout(new HorizontalLayout(HorizontalLayout.Alignment.RIGHT).setSpacing(2)) - .addElements(null, videoWidth, new GuiLabel().setText("*"), videoHeight); + .addElements(new HorizontalLayout.Data(0.5), videoWidth, new GuiLabel().setText("*"), videoHeight); public final GuiNumberField bitRateField = new GuiNumberField().setValue(10).setSize(50, 20); public final GuiDropdownMenu bitRateUnit = new GuiDropdownMenu() @@ -130,7 +130,7 @@ public class GuiRenderSettings extends GuiScreen implements Closeable { new GuiPanel().addElements(null, bitRateField, bitRateUnit).setLayout(new HorizontalLayout()), frameRateSlider).setLayout(new HorizontalLayout(HorizontalLayout.Alignment.RIGHT).setSpacing(3)), new GuiLabel().setI18nText("replaymod.gui.rendersettings.outputfile"), outputFileButton) - .setLayout(new GridLayout().setCellsEqualSize(false).setColumns(2).setSpacingX(5).setSpacingY(3)); + .setLayout(new GridLayout().setCellsEqualSize(false).setColumns(2).setSpacingX(5).setSpacingY(5)); public final GuiCheckbox nametagCheckbox = new GuiCheckbox() .setI18nLabel("replaymod.gui.rendersettings.nametags"); @@ -284,6 +284,15 @@ public class GuiRenderSettings extends GuiScreen implements Closeable { videoHeight.setTextColor(Colors.RED); } + // Enable/Disable bitrate input field and dropdown + if (encodingPresetDropdown.getSelectedValue().hasBitrateSetting()) { + bitRateField.setEnabled(); + bitRateUnit.setEnabled(); + } else { + bitRateField.setDisabled(); + bitRateUnit.setDisabled(); + } + // Enable/Disable camera stabilization checkboxes switch (renderMethodDropdown.getSelectedValue()) { case CUBIC: diff --git a/src/main/java/com/replaymod/replay/ReplayHandler.java b/src/main/java/com/replaymod/replay/ReplayHandler.java index 936395ab..deb53913 100755 --- a/src/main/java/com/replaymod/replay/ReplayHandler.java +++ b/src/main/java/com/replaymod/replay/ReplayHandler.java @@ -62,7 +62,7 @@ public class ReplayHandler { */ private boolean suppressCameraMovements; - private final Set markers; + private final List markers; private final GuiReplayOverlay overlay; @@ -81,7 +81,7 @@ public class ReplayHandler { MinecraftForge.EVENT_BUS.post(new ReplayOpenEvent.Pre(this)); - markers = new HashSet<>(replayFile.getMarkers().or(Collections.emptySet())); + markers = new ArrayList<>(replayFile.getMarkers().or(Collections.emptySet())); replaySender = new ReplaySender(this, replayFile, asyncMode); @@ -188,9 +188,9 @@ public class ReplayHandler { /** * Returns all markers. * When changed, {@link #saveMarkers()} should be called to save the changes. - * @return Set of markers + * @return Collection of markers in no particular order */ - public Set getMarkers() { + public Collection getMarkers() { return markers; } @@ -199,7 +199,7 @@ public class ReplayHandler { */ public void saveMarkers() { try { - replayFile.writeMarkers(markers); + replayFile.writeMarkers(new HashSet<>(markers)); } catch (IOException e) { e.printStackTrace(); } diff --git a/src/main/java/com/replaymod/replay/camera/CameraEntity.java b/src/main/java/com/replaymod/replay/camera/CameraEntity.java index 3701f4e8..e038ab92 100755 --- a/src/main/java/com/replaymod/replay/camera/CameraEntity.java +++ b/src/main/java/com/replaymod/replay/camera/CameraEntity.java @@ -10,6 +10,7 @@ import lombok.Getter; import lombok.Setter; import net.minecraft.block.material.Material; import net.minecraft.client.Minecraft; +import net.minecraft.client.entity.AbstractClientPlayer; import net.minecraft.client.entity.EntityPlayerSP; import net.minecraft.client.network.NetHandlerPlayClient; import net.minecraft.client.settings.KeyBinding; @@ -237,6 +238,14 @@ public class CameraEntity extends EntityPlayerSP { return ReplayModReplay.instance.getReplayHandler().isCameraView(); // Make sure we're treated as spectator } + @Override + public boolean shouldRenderInPass(int pass) { + // Never render the camera + // This is necessary to hide the player head in third person mode and to not + // cause any unwanted shadows when rendering with shaders. + return false; + } + @Override public ResourceLocation getLocationSkin() { Entity view = mc.getRenderViewEntity(); @@ -246,6 +255,15 @@ public class CameraEntity extends EntityPlayerSP { return super.getLocationSkin(); } + @Override + public String getSkinType() { + Entity view = mc.getRenderViewEntity(); + if (view != this && view instanceof AbstractClientPlayer) { + return ((AbstractClientPlayer) view).getSkinType(); + } + return super.getSkinType(); + } + @Override public float getSwingProgress(float renderPartialTicks) { Entity view = mc.getRenderViewEntity(); diff --git a/src/main/java/com/replaymod/replay/gui/overlay/GuiEditMarkerPopup.java b/src/main/java/com/replaymod/replay/gui/overlay/GuiEditMarkerPopup.java index 27c46da6..23e9e5b5 100644 --- a/src/main/java/com/replaymod/replay/gui/overlay/GuiEditMarkerPopup.java +++ b/src/main/java/com/replaymod/replay/gui/overlay/GuiEditMarkerPopup.java @@ -5,14 +5,17 @@ import com.replaymod.replay.ReplayHandler; import de.johni0702.minecraft.gui.container.GuiContainer; import de.johni0702.minecraft.gui.container.GuiPanel; import de.johni0702.minecraft.gui.element.*; +import de.johni0702.minecraft.gui.function.Typeable; import de.johni0702.minecraft.gui.layout.GridLayout; import de.johni0702.minecraft.gui.layout.HorizontalLayout; import de.johni0702.minecraft.gui.layout.VerticalLayout; import de.johni0702.minecraft.gui.popup.AbstractGuiPopup; import de.johni0702.minecraft.gui.utils.Colors; import com.replaymod.replaystudio.data.Marker; +import org.lwjgl.input.Keyboard; +import org.lwjgl.util.ReadablePoint; -public class GuiEditMarkerPopup extends AbstractGuiPopup { +public class GuiEditMarkerPopup extends AbstractGuiPopup implements Typeable { private final ReplayHandler replayHandler; private final Marker marker; @@ -107,4 +110,13 @@ public class GuiEditMarkerPopup extends AbstractGuiPopup { protected GuiEditMarkerPopup getThis() { return this; } + + @Override + public boolean typeKey(ReadablePoint mousePosition, int keyCode, char keyChar, boolean ctrlDown, boolean shiftDown) { + if (keyCode == Keyboard.KEY_ESCAPE) { + cancelButton.onClick(); + return true; + } + return false; + } } diff --git a/src/main/java/com/replaymod/replay/gui/overlay/GuiMarkerTimeline.java b/src/main/java/com/replaymod/replay/gui/overlay/GuiMarkerTimeline.java index 229237f0..04cf7391 100644 --- a/src/main/java/com/replaymod/replay/gui/overlay/GuiMarkerTimeline.java +++ b/src/main/java/com/replaymod/replay/gui/overlay/GuiMarkerTimeline.java @@ -133,6 +133,8 @@ public class GuiMarkerTimeline extends AbstractGuiTimeline im replayHandler.doJump(marker.getTime(), false); } return true; + } else { + selectedMarker = null; } return super.mouseClick(position, button); } diff --git a/src/main/java/com/replaymod/replay/gui/overlay/GuiReplayOverlay.java b/src/main/java/com/replaymod/replay/gui/overlay/GuiReplayOverlay.java index fd2be83e..eeba30db 100644 --- a/src/main/java/com/replaymod/replay/gui/overlay/GuiReplayOverlay.java +++ b/src/main/java/com/replaymod/replay/gui/overlay/GuiReplayOverlay.java @@ -7,8 +7,10 @@ import de.johni0702.minecraft.gui.GuiRenderer; import de.johni0702.minecraft.gui.RenderInfo; import de.johni0702.minecraft.gui.container.AbstractGuiOverlay; import de.johni0702.minecraft.gui.container.GuiPanel; +import de.johni0702.minecraft.gui.element.GuiElement; import de.johni0702.minecraft.gui.element.GuiSlider; import de.johni0702.minecraft.gui.element.GuiTexturedButton; +import de.johni0702.minecraft.gui.element.GuiTooltip; import de.johni0702.minecraft.gui.element.advanced.IGuiTimeline; import de.johni0702.minecraft.gui.layout.CustomLayout; import de.johni0702.minecraft.gui.layout.HorizontalLayout; @@ -27,8 +29,20 @@ public class GuiReplayOverlay extends AbstractGuiOverlay { public final GuiPanel topPanel = new GuiPanel(this) .setLayout(new HorizontalLayout(HorizontalLayout.Alignment.LEFT).setSpacing(5)); - public final GuiTexturedButton playPauseButton = new GuiTexturedButton().setSize(20, 20) - .setTexture(ReplayMod.TEXTURE, TEXTURE_SIZE); + public final GuiTexturedButton playPauseButton = new GuiTexturedButton() { + @Override + public GuiElement getTooltip(RenderInfo renderInfo) { + GuiTooltip tooltip = (GuiTooltip) super.getTooltip(renderInfo); + if (tooltip != null) { + if (getTextureNormal().getY() == 0) { // Play button + tooltip.setI18nText("replaymod.gui.ingame.menu.unpause"); + } else { // Pause button + tooltip.setI18nText("replaymod.gui.ingame.menu.pause"); + } + } + return tooltip; + } + }.setSize(20, 20).setTexture(ReplayMod.TEXTURE, TEXTURE_SIZE).setTooltip(new GuiTooltip()); public final GuiSlider speedSlider = new GuiSlider().setSize(100, 20).setSteps(37); // 0.0 is not included public final GuiMarkerTimeline timeline; diff --git a/src/main/java/com/replaymod/replay/gui/screen/GuiReplayViewer.java b/src/main/java/com/replaymod/replay/gui/screen/GuiReplayViewer.java index 307238f3..b9ba3961 100755 --- a/src/main/java/com/replaymod/replay/gui/screen/GuiReplayViewer.java +++ b/src/main/java/com/replaymod/replay/gui/screen/GuiReplayViewer.java @@ -101,6 +101,8 @@ public class GuiReplayViewer extends GuiScreen { }).onSelectionDoubleClicked(() -> { if (this.loadButton.isEnabled()) { this.loadButton.onClick(); + // Disable load button to prevent the player from opening the replay twice at the same time + this.loadButton.setDisabled(); } }).setDrawShadow(true).setDrawSlider(true); diff --git a/src/main/java/com/replaymod/simplepathing/gui/GuiEditKeyframe.java b/src/main/java/com/replaymod/simplepathing/gui/GuiEditKeyframe.java index 335d79cf..edb160a5 100644 --- a/src/main/java/com/replaymod/simplepathing/gui/GuiEditKeyframe.java +++ b/src/main/java/com/replaymod/simplepathing/gui/GuiEditKeyframe.java @@ -13,16 +13,20 @@ import de.johni0702.minecraft.gui.element.GuiButton; import de.johni0702.minecraft.gui.element.GuiLabel; import de.johni0702.minecraft.gui.element.GuiNumberField; import de.johni0702.minecraft.gui.element.IGuiLabel; +import de.johni0702.minecraft.gui.function.Typeable; import de.johni0702.minecraft.gui.layout.GridLayout; import de.johni0702.minecraft.gui.layout.HorizontalLayout; import de.johni0702.minecraft.gui.layout.VerticalLayout; import de.johni0702.minecraft.gui.popup.AbstractGuiPopup; import de.johni0702.minecraft.gui.utils.Colors; +import de.johni0702.minecraft.gui.utils.Consumer; import org.apache.commons.lang3.tuple.Triple; +import org.lwjgl.input.Keyboard; +import org.lwjgl.util.ReadablePoint; import static de.johni0702.minecraft.gui.utils.Utils.link; -public abstract class GuiEditKeyframe> extends AbstractGuiPopup { +public abstract class GuiEditKeyframe> extends AbstractGuiPopup implements Typeable { private static GuiNumberField newGuiNumberField() { return new GuiNumberField().setPrecision(0).setValidateOnFocusChange(true); } @@ -68,9 +72,10 @@ public abstract class GuiEditKeyframe> extends Abst this.path = path; long time = keyframe.getTime(); - timeMinField.setValue(time / 1000 / 60); - timeSecField.setValue(time / 1000 % 60); - timeMSecField.setValue(time % 1000); + Consumer updateSaveButtonState = s -> saveButton.setEnabled(canSave()); + timeMinField.setValue(time / 1000 / 60).onTextChanged(updateSaveButtonState); + timeSecField.setValue(time / 1000 % 60).onTextChanged(updateSaveButtonState); + timeMSecField.setValue(time % 1000).onTextChanged(updateSaveButtonState); title.setI18nText("replaymod.gui.editkeyframe.title." + type); saveButton.onClick(() -> { @@ -84,6 +89,23 @@ public abstract class GuiEditKeyframe> extends Abst }); } + private boolean canSave() { + long newTime = (timeMinField.getInteger() * 60 + timeSecField.getInteger()) * 1000 + timeMSecField.getInteger(); + if (newTime != keyframe.getTime() && path.getKeyframe(newTime) != null) { + return false; + } + return true; + } + + @Override + public boolean typeKey(ReadablePoint mousePosition, int keyCode, char keyChar, boolean ctrlDown, boolean shiftDown) { + if (keyCode == Keyboard.KEY_ESCAPE) { + cancelButton.onClick(); + return true; + } + return false; + } + @Override public void open() { super.open(); diff --git a/src/main/java/com/replaymod/simplepathing/gui/GuiKeyframeTimeline.java b/src/main/java/com/replaymod/simplepathing/gui/GuiKeyframeTimeline.java index e9017265..72f1368c 100644 --- a/src/main/java/com/replaymod/simplepathing/gui/GuiKeyframeTimeline.java +++ b/src/main/java/com/replaymod/simplepathing/gui/GuiKeyframeTimeline.java @@ -121,24 +121,42 @@ public class GuiKeyframeTimeline extends AbstractGuiTimeline= endTime || endFrameTime <= startTime) { - continue; // Segment out of display range - } + drawQuadOnSegment(renderer, visibleWidth, segment, BORDER_TOP + 1, 0xFF0088FF); + } - double relativeStart = startFrameTime - startTime; - double relativeEnd = endFrameTime - startTime; - int startX = BORDER_LEFT + Math.max(0, (int) (relativeStart / visibleTime * visibleWidth) + KEYFRAME_SIZE / 2 + 1); - int endX = BORDER_LEFT + Math.min(visibleWidth, (int) (relativeEnd / visibleTime * visibleWidth) - KEYFRAME_SIZE / 2); - if (startX < endX) { - renderer.drawRect(startX + 1, BORDER_TOP + 1, endX - startX - 2, KEYFRAME_SIZE - 2, 0xFF0088FF); + // Draw red quads on time path segments that would require time going backwards + for (PathSegment segment : mod.getCurrentTimeline().getPaths().get(GuiPathing.TIME_PATH).getSegments()) { + long startTimestamp = segment.getStartKeyframe().getValue(TimestampProperty.PROPERTY).orElseThrow(IllegalStateException::new); + long endTimestamp = segment.getEndKeyframe().getValue(TimestampProperty.PROPERTY).orElseThrow(IllegalStateException::new); + if (endTimestamp >= startTimestamp) { + continue; // All is fine, time is not moving backwards } + drawQuadOnSegment(renderer, visibleWidth, segment, BORDER_TOP + KEYFRAME_SIZE + 1, 0xFFFF0000); } super.drawTimelineCursor(renderer, size); } + private void drawQuadOnSegment(GuiRenderer renderer, int visibleWidth, PathSegment segment, int y, int color) { + int startTime = getOffset(); + int visibleTime = (int) (getZoom() * getLength()); + int endTime = getOffset() + visibleTime; + + long startFrameTime = segment.getStartKeyframe().getTime(); + long endFrameTime = segment.getEndKeyframe().getTime(); + if (startFrameTime >= endTime || endFrameTime <= startTime) { + return; // Segment out of display range + } + + double relativeStart = startFrameTime - startTime; + double relativeEnd = endFrameTime - startTime; + int startX = BORDER_LEFT + Math.max(0, (int) (relativeStart / visibleTime * visibleWidth) + KEYFRAME_SIZE / 2 + 1); + int endX = BORDER_LEFT + Math.min(visibleWidth, (int) (relativeEnd / visibleTime * visibleWidth) - KEYFRAME_SIZE / 2); + if (startX < endX) { + renderer.drawRect(startX + 1, y, endX - startX - 2, KEYFRAME_SIZE - 2, color); + } + } + /** * Returns the keyframe at the specified position. * @param position The raw position @@ -243,6 +261,15 @@ public class GuiKeyframeTimeline extends AbstractGuiTimeline future = player.start(timeline); + // Start from cursor time unless the control key is pressed (then start from beginning) + int startTime = Keyboard.isKeyDown(Keyboard.KEY_LCONTROL)? 0 : GuiPathing.this.timeline.getCursorPosition(); + ListenableFuture future = player.start(timeline, startTime); overlay.setCloseable(false); overlay.setMouseVisible(true); Futures.addCallback(future, new FutureCallback() { @@ -403,6 +455,15 @@ public class GuiPathing { }); }); + core.getKeyBindingRegistry().registerRaw(Keyboard.KEY_DELETE, () -> { + if (!overlay.isVisible()) { + return; + } + if (mod.getSelectedKeyframe() != null) { + updateKeyframe(mod.getSelectedKeyframe().getValue(TimestampProperty.PROPERTY).isPresent()); + } + }); + // Start loading entity tracker entityTrackerFuture = SettableFuture.create(); new Thread(() -> { @@ -429,10 +490,27 @@ public class GuiPathing { }).start(); } - private void preparePathsForPlayback() { + private boolean preparePathsForPlayback() { Timeline timeline = mod.getCurrentTimeline(); timeline.getPaths().get(TIME_PATH).updateAll(); timeline.getPaths().get(POSITION_PATH).updateAll(); + + // Make sure time keyframes's values are monotonically increasing + int lastTime = 0; + for (Keyframe keyframe : timeline.getPaths().get(TIME_PATH).getKeyframes()) { + int time = keyframe.getValue(TimestampProperty.PROPERTY).orElseThrow(IllegalStateException::new); + if (time < lastTime) { + // We are going backwards in time + GuiInfoPopup.open(replayHandler.getOverlay(), + "replaymod.error.negativetime1", + "replaymod.error.negativetime2", + "replaymod.error.negativetime3"); + return false; + } + lastTime = time; + } + + return true; } public void zoomTimeline(double factor) { diff --git a/src/main/resources/assets/replaymod/lang/en_US.lang b/src/main/resources/assets/replaymod/lang/en_US.lang index 1c660296..ecdf54ef 100644 --- a/src/main/resources/assets/replaymod/lang/en_US.lang +++ b/src/main/resources/assets/replaymod/lang/en_US.lang @@ -485,6 +485,9 @@ replaymod.gui.objects=Custom Objects #Errors replaymod.error.unknownrestriction1=This replay cannot be played with your current version. replaymod.error.unknownrestriction2=It tried to enforce %s which is unknown. +replaymod.error.negativetime1=Some of your time keyframes are out of order. +replaymod.error.negativetime2=Going backwards in time is not supported. +replaymod.error.negativetime3=The invalid parts are marked in red. #Replay Mod Incompatibility Warning replaymod.gui.modwarning.title=Incompatibility detected