From cd162113094b9e4c575788a934b7a2d5f4bc84b9 Mon Sep 17 00:00:00 2001 From: CrushedPixel Date: Fri, 13 Jan 2017 12:05:07 +0100 Subject: [PATCH 01/19] Register new Property to imply that the path segment following a keyframe has a user-set interpolator instead of the default Modified Keyframe GUI to allow for custom Path interpolator --- .../ExplicitInterpolationProperty.java | 43 ++++ .../simplepathing/ReplayModSimplePathing.java | 37 ++- .../com/replaymod/simplepathing/Setting.java | 16 +- .../simplepathing/gui/GuiEditKeyframe.java | 220 +++++++++++++++++- .../simplepathing/gui/GuiPathing.java | 123 ++++++++-- .../assets/replaymod/lang/en_US.lang | 9 + 6 files changed, 417 insertions(+), 31 deletions(-) create mode 100644 src/main/java/com/replaymod/pathing/properties/ExplicitInterpolationProperty.java diff --git a/src/main/java/com/replaymod/pathing/properties/ExplicitInterpolationProperty.java b/src/main/java/com/replaymod/pathing/properties/ExplicitInterpolationProperty.java new file mode 100644 index 00000000..6443e91b --- /dev/null +++ b/src/main/java/com/replaymod/pathing/properties/ExplicitInterpolationProperty.java @@ -0,0 +1,43 @@ +package com.replaymod.pathing.properties; + +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import com.replaymod.replaystudio.pathing.property.AbstractProperty; +import com.replaymod.replaystudio.pathing.property.PropertyPart; +import lombok.NonNull; + +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; + +/** + * Property indicating that the next path segment's interpolator is fixed + */ +public class ExplicitInterpolationProperty extends AbstractProperty { + public static final ExplicitInterpolationProperty PROPERTY = new ExplicitInterpolationProperty(); + + private ExplicitInterpolationProperty() { + super("interpolationFixed", "", null, new Object()); + } + + @Override + public Collection> getParts() { + return Collections.emptyList(); + } + + @Override + public void applyToGame(Object value, @NonNull Object replayHandler) { + // dummy property, do nothing + } + + @Override + public void toJson(JsonWriter writer, Object value) throws IOException { + writer.nullValue(); + } + + @Override + public Void fromJson(JsonReader reader) throws IOException { + reader.nextNull(); + return null; + } +} diff --git a/src/main/java/com/replaymod/simplepathing/ReplayModSimplePathing.java b/src/main/java/com/replaymod/simplepathing/ReplayModSimplePathing.java index 8a884a04..d55ae73c 100644 --- a/src/main/java/com/replaymod/simplepathing/ReplayModSimplePathing.java +++ b/src/main/java/com/replaymod/simplepathing/ReplayModSimplePathing.java @@ -5,6 +5,7 @@ import com.google.gson.stream.JsonWriter; import com.replaymod.core.ReplayMod; import com.replaymod.core.events.SettingsChangedEvent; import com.replaymod.pathing.properties.CameraProperties; +import com.replaymod.pathing.properties.ExplicitInterpolationProperty; import com.replaymod.pathing.properties.SpectatorProperty; import com.replaymod.pathing.properties.TimestampProperty; import com.replaymod.replay.events.ReplayCloseEvent; @@ -16,6 +17,7 @@ import com.replaymod.replaystudio.pathing.interpolation.Interpolator; import com.replaymod.replaystudio.pathing.interpolation.LinearInterpolator; import com.replaymod.replaystudio.pathing.path.Keyframe; import com.replaymod.replaystudio.pathing.path.Timeline; +import com.replaymod.replaystudio.pathing.property.Property; import com.replaymod.simplepathing.gui.GuiPathing; import com.replaymod.simplepathing.preview.PathPreview; import net.minecraftforge.fml.common.FMLCommonHandler; @@ -24,7 +26,10 @@ import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; import org.apache.logging.log4j.Logger; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringReader; @Mod(modid = ReplayModSimplePathing.MOD_ID, version = "@MOD_VERSION@", @@ -68,7 +73,7 @@ public class ReplayModSimplePathing implements PathingRegistry { @SubscribeEvent public void onSettingsChanged(SettingsChangedEvent event) { - if (event.getKey() == Setting.LINEAR_INTERPOLATION) { + if (event.getKey() == Setting.DEFAULT_INTERPOLATION) { if (currentTimeline != null && guiPathing != null) { currentTimeline.applyChange(guiPathing.updateInterpolators()); } @@ -105,6 +110,7 @@ public class ReplayModSimplePathing implements PathingRegistry { timeline.registerProperty(CameraProperties.POSITION); timeline.registerProperty(CameraProperties.ROTATION); timeline.registerProperty(SpectatorProperty.PROPERTY); + timeline.registerProperty(ExplicitInterpolationProperty.PROPERTY); return timeline; } @@ -134,6 +140,35 @@ public class ReplayModSimplePathing implements PathingRegistry { } } + /** + * Clones an interpolator by de- and reserializing it. + * @param interpolator The interpolator to clone + * @return The cloned interpolator + * @throws IOException + */ + public Interpolator cloneInterpolator(Interpolator interpolator) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + PrintWriter printWriter = new PrintWriter(baos); + + JsonWriter jsonWriter = new JsonWriter(printWriter); + jsonWriter.beginArray(); + serializeInterpolator(jsonWriter, interpolator); + jsonWriter.endArray(); + jsonWriter.flush(); + + String json = baos.toString(); + + JsonReader jsonReader = new JsonReader(new StringReader(json)); + jsonReader.beginArray(); + Interpolator cloned = deserializeInterpolator(jsonReader); + + for (Property p : interpolator.getKeyframeProperties()) { + cloned.registerProperty(p); + } + + return cloned; + } + public ReplayMod getCore() { return core; } diff --git a/src/main/java/com/replaymod/simplepathing/Setting.java b/src/main/java/com/replaymod/simplepathing/Setting.java index b1e99e17..328714b5 100644 --- a/src/main/java/com/replaymod/simplepathing/Setting.java +++ b/src/main/java/com/replaymod/simplepathing/Setting.java @@ -2,9 +2,23 @@ package com.replaymod.simplepathing; import com.replaymod.core.SettingsRegistry; +import java.util.ArrayList; + public final class Setting extends SettingsRegistry.SettingKeys { public static final Setting PATH_PREVIEW = make("pathpreview", "pathpreview", true); - public static final Setting LINEAR_INTERPOLATION = make("linear_interpolation", "linearinterpolation", false); + public static final SettingsRegistry.MultipleChoiceSettingKeys DEFAULT_INTERPOLATION = + new SettingsRegistry.MultipleChoiceSettingKeys<>( + "simplepathing", "interpolator", "replaymod.gui.settings.interpolator", + "replaymod.gui.editkeyframe.interpolator.cubic.name"); + + static { + DEFAULT_INTERPOLATION.setChoices(new ArrayList() { + { + add("replaymod.gui.editkeyframe.interpolator.cubic.name"); + add("replaymod.gui.editkeyframe.interpolator.linear.name"); + } + }); + } private static Setting make(String key, String displayName, T defaultValue) { return new Setting<>(key, displayName, defaultValue); diff --git a/src/main/java/com/replaymod/simplepathing/gui/GuiEditKeyframe.java b/src/main/java/com/replaymod/simplepathing/gui/GuiEditKeyframe.java index bc61f618..d1c57a8f 100644 --- a/src/main/java/com/replaymod/simplepathing/gui/GuiEditKeyframe.java +++ b/src/main/java/com/replaymod/simplepathing/gui/GuiEditKeyframe.java @@ -1,18 +1,24 @@ package com.replaymod.simplepathing.gui; import com.replaymod.pathing.properties.CameraProperties; +import com.replaymod.pathing.properties.ExplicitInterpolationProperty; import com.replaymod.pathing.properties.TimestampProperty; import com.replaymod.replay.ReplayModReplay; import com.replaymod.replaystudio.pathing.change.Change; import com.replaymod.replaystudio.pathing.change.CombinedChange; +import com.replaymod.replaystudio.pathing.change.SetInterpolator; import com.replaymod.replaystudio.pathing.change.UpdateKeyframeProperties; +import com.replaymod.replaystudio.pathing.interpolation.CubicSplineInterpolator; +import com.replaymod.replaystudio.pathing.interpolation.Interpolator; +import com.replaymod.replaystudio.pathing.interpolation.LinearInterpolator; import com.replaymod.replaystudio.pathing.path.Keyframe; import com.replaymod.replaystudio.pathing.path.Path; +import com.replaymod.replaystudio.pathing.path.PathSegment; +import com.replaymod.simplepathing.Setting; +import com.replaymod.simplepathing.gui.GuiEditKeyframe.Position.InterpolationPanel.InterpolatorType; import de.johni0702.minecraft.gui.container.GuiPanel; -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.element.*; +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.layout.HorizontalLayout; @@ -20,10 +26,19 @@ 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 lombok.AllArgsConstructor; +import lombok.Getter; +import net.minecraft.client.resources.I18n; import org.apache.commons.lang3.tuple.Triple; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.lwjgl.input.Keyboard; import org.lwjgl.util.ReadablePoint; +import java.util.Map; + +import static com.replaymod.simplepathing.gui.GuiEditKeyframe.Position.InterpolationPanel.InterpolatorSettingsPanel.CubicInterpolatorSettingsPanel; +import static com.replaymod.simplepathing.gui.GuiEditKeyframe.Position.InterpolationPanel.InterpolatorSettingsPanel.LinearInterpolatorSettingsPanel; import static de.johni0702.minecraft.gui.utils.Utils.link; public abstract class GuiEditKeyframe> extends AbstractGuiPopup implements Typeable { @@ -31,6 +46,10 @@ public abstract class GuiEditKeyframe> extends Abst return new GuiNumberField().setPrecision(0).setValidateOnFocusChange(true); } + protected static final Logger logger = LogManager.getLogger(); + + protected final GuiPathing guiPathing; + protected final Keyframe keyframe; protected final Path path; @@ -68,6 +87,7 @@ public abstract class GuiEditKeyframe> extends Abst public GuiEditKeyframe(GuiPathing gui, Path path, Keyframe keyframe, String type) { super(ReplayModReplay.instance.getReplayHandler().getOverlay()); + this.guiPathing = gui; this.keyframe = keyframe; this.path = path; @@ -187,8 +207,11 @@ public abstract class GuiEditKeyframe> extends Abst public final GuiNumberField pitchField = newGuiNumberField().setSize(60, 20).setPrecision(5); public final GuiNumberField rollField = newGuiNumberField().setSize(60, 20).setPrecision(5); + public final InterpolationPanel interpolationPanel = new InterpolationPanel(guiPathing); + { - inputs.setLayout(new GridLayout().setCellsEqualSize(false).setColumns(4).setSpacingX(3).setSpacingY(5)) + GuiPanel positionInputs = new GuiPanel() + .setLayout(new GridLayout().setCellsEqualSize(false).setColumns(4).setSpacingX(3).setSpacingY(5)) .addElements(new GridLayout.Data(1, 0.5), new GuiLabel().setI18nText("replaymod.gui.editkeyframe.xpos"), xField, new GuiLabel().setI18nText("replaymod.gui.editkeyframe.camyaw"), yawField, @@ -196,6 +219,9 @@ public abstract class GuiEditKeyframe> extends Abst new GuiLabel().setI18nText("replaymod.gui.editkeyframe.campitch"), pitchField, new GuiLabel().setI18nText("replaymod.gui.editkeyframe.zpos"), zField, new GuiLabel().setI18nText("replaymod.gui.editkeyframe.camroll"), rollField); + + inputs.setLayout(new VerticalLayout().setSpacing(10)).addElements(new VerticalLayout.Data(0.5, false), + positionInputs, interpolationPanel); } public Position(GuiPathing gui, Path path, Keyframe keyframe) { @@ -219,17 +245,193 @@ public abstract class GuiEditKeyframe> extends Abst @Override protected Change save() { - Change change = UpdateKeyframeProperties.create(path, keyframe) + Change setInterpolatorChange = null; + + UpdateKeyframeProperties.Builder builder = UpdateKeyframeProperties.create(path, keyframe) .setValue(CameraProperties.POSITION, Triple.of(xField.getDouble(), yField.getDouble(), zField.getDouble())) .setValue(CameraProperties.ROTATION, Triple.of(yawField.getFloat(), pitchField.getFloat(), rollField.getFloat())) - .done(); - change.apply(path.getTimeline()); - return change; + .removeProperty(ExplicitInterpolationProperty.PROPERTY); + + // if the interpolator is not the default, set the ExplicitInterpolationProperty flag + if (interpolationPanel.getInterpolatorType() != InterpolatorType.DEFAULT) { + PathSegment toModify = null; + for (PathSegment segment : path.getSegments()) { + if (segment.getStartKeyframe() == keyframe) { + toModify = segment; + break; + } + } + + if (toModify != null) { + builder.setValue(ExplicitInterpolationProperty.PROPERTY, new Object()); + + Interpolator interpolator = interpolationPanel.getInterpolatorSettingsPanel().createInterpolator(); + interpolator.registerProperty(CameraProperties.POSITION); + interpolator.registerProperty(CameraProperties.ROTATION); + + setInterpolatorChange = SetInterpolator.create(toModify, interpolator); + } else { + logger.warn("The Path segment to modify was not found. Setting interpolator to default."); + } + } + + Change keyframePropertiesChange = builder.done(); + keyframePropertiesChange.apply(path.getTimeline()); + + if (setInterpolatorChange == null) { + return keyframePropertiesChange; + } else { + setInterpolatorChange.apply(path.getTimeline()); + guiPathing.updateInterpolators(); + path.updateAll(); + } + + return CombinedChange.createFromApplied(keyframePropertiesChange, setInterpolatorChange); } @Override protected Position getThis() { return this; } + + public static class InterpolationPanel extends de.johni0702.minecraft.gui.container.AbstractGuiContainer { + + private final GuiPathing guiPathing; + + @Getter + private InterpolatorSettingsPanel interpolatorSettingsPanel; + + private GuiDropdownMenu dropdown; + + @AllArgsConstructor + public enum InterpolatorType { + DEFAULT("default", null), + CUBIC("cubic", CubicSplineInterpolator.class), + LINEAR("linear", LinearInterpolator.class); + + private String localizationKey; + + @Getter + private Class interpolatorClass; + + @Override + public String toString() { + return I18n.format(String.format("replaymod.gui.editkeyframe.interpolator.%1$s.name", localizationKey)); + } + + public String getI18nDescription() { + return String.format("replaymod.gui.editkeyframe.interpolator.%1$s.desc", localizationKey); + } + + public static InterpolatorType fromString(String string) { + for (InterpolatorType t : values()) { + if (t.toString().equals(string)) return t; + } + return CUBIC; //the default + } + + } + + public InterpolationPanel(GuiPathing guiPathing) { + this.guiPathing = guiPathing; + + setLayout(new VerticalLayout()); + + dropdown = new GuiDropdownMenu().setValues(InterpolatorType.values()).setHeight(20); + dropdown.onSelection((index) -> setSettingsPanel(dropdown.getSelectedValue())); + + // set hover tooltips + for (Map.Entry e : dropdown.getDropdownEntries().entrySet()) { + e.getValue().setTooltip(new GuiTooltip().setI18nText(e.getKey().getI18nDescription())); + } + + GuiPanel dropdownPanel = new GuiPanel() + .setLayout(new GridLayout().setCellsEqualSize(false).setColumns(2).setSpacingX(3).setSpacingY(5)) + .addElements(new GridLayout.Data(1, 0.5), + new GuiLabel().setI18nText("replaymod.gui.editkeyframe.interpolator"), dropdown); + + + addElements(new VerticalLayout.Data(0.5, false), dropdownPanel); + + dropdown.onSelection(0); // trigger the callback once to display settings panel + } + + public void setSettingsPanel(InterpolatorType type) { + removeElement(this.interpolatorSettingsPanel); + + InterpolatorSettingsPanel settingsPanel = null; + switch (getInterpolatorTypeNoDefault(type)) { + case CUBIC: + settingsPanel = new CubicInterpolatorSettingsPanel(); + break; + case LINEAR: + settingsPanel = new LinearInterpolatorSettingsPanel(); + break; + } + + addElements(new GridLayout.Data(0.5, 0.5), settingsPanel); + + this.interpolatorSettingsPanel = settingsPanel; + } + + protected InterpolatorType getInterpolatorTypeNoDefault(InterpolatorType interpolatorType) { + if (interpolatorType == InterpolatorType.DEFAULT || interpolatorType == null) { + InterpolatorType defaultType = InterpolatorType.fromString( + guiPathing.getMod().getCore().getSettingsRegistry().get(Setting.DEFAULT_INTERPOLATION)); + return defaultType; + } + return interpolatorType; + } + + public InterpolatorType getInterpolatorType() { + return dropdown.getSelectedValue(); + } + + @Override + protected InterpolationPanel getThis() { + return this; + } + + public static abstract class InterpolatorSettingsPanel extends de.johni0702.minecraft.gui.container.GuiPanel { + + public abstract void loadSettings(I interpolator); + + public abstract I createInterpolator(); + + public static class CubicInterpolatorSettingsPanel extends InterpolatorSettingsPanel { + + @Override + public void loadSettings(CubicSplineInterpolator interpolator) { + } + + @Override + public CubicSplineInterpolator createInterpolator() { + return new CubicSplineInterpolator(); + } + + @Override + protected InterpolatorSettingsPanel getThis() { + return this; + } + } + + public static class LinearInterpolatorSettingsPanel extends InterpolatorSettingsPanel { + + @Override + public void loadSettings(LinearInterpolator interpolator) { + } + + @Override + public LinearInterpolator createInterpolator() { + return new LinearInterpolator(); + } + + @Override + protected InterpolatorSettingsPanel getThis() { + return this; + } + } + } + } } } diff --git a/src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java b/src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java index 8eb27501..ed3a1134 100644 --- a/src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java +++ b/src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java @@ -5,9 +5,11 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; import com.replaymod.core.ReplayMod; +import com.replaymod.core.utils.Utils; import com.replaymod.pathing.gui.GuiKeyframeRepository; import com.replaymod.pathing.player.RealtimeTimelinePlayer; import com.replaymod.pathing.properties.CameraProperties; +import com.replaymod.pathing.properties.ExplicitInterpolationProperty; import com.replaymod.pathing.properties.SpectatorProperty; import com.replaymod.pathing.properties.TimestampProperty; import com.replaymod.render.gui.GuiRenderSettings; @@ -28,6 +30,7 @@ import com.replaymod.replaystudio.util.EntityPositionTracker; import com.replaymod.replaystudio.util.Location; import com.replaymod.simplepathing.ReplayModSimplePathing; import com.replaymod.simplepathing.Setting; +import com.replaymod.simplepathing.gui.GuiEditKeyframe.Position.InterpolationPanel.InterpolatorType; import de.johni0702.minecraft.gui.GuiRenderer; import de.johni0702.minecraft.gui.RenderInfo; import de.johni0702.minecraft.gui.container.GuiContainer; @@ -57,10 +60,7 @@ import org.lwjgl.util.WritablePoint; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Optional; +import java.util.*; import java.util.function.Consumer; import static com.replaymod.core.utils.Utils.error; @@ -637,11 +637,48 @@ public class GuiPathing { } public Change updateInterpolators() { - boolean linearInterpolation = mod.getCore().getSettingsRegistry().get(Setting.LINEAR_INTERPOLATION); + InterpolatorType type = InterpolatorType.fromString(mod.getCore().getSettingsRegistry().get(Setting.DEFAULT_INTERPOLATION)); + + Interpolator defaultInterpolator = null; + + // Iterate over all existing segments until the first interpolator + // of the same type without the fixed flag is found and clone it. + // This way, the default interpolator's settings can be saved on a per-camera-path basis. + // This is useful for interpolators that have customizable properties. + Collection pathSegments = mod.getCurrentTimeline().getPaths().get(POSITION_PATH).getSegments(); + + for (PathSegment segment : pathSegments) { + if (segment.getStartKeyframe().getValue(ExplicitInterpolationProperty.PROPERTY).isPresent()) continue; + if (segment.getInterpolator() == null) continue; + if (type == InterpolatorType.DEFAULT || type.getInterpolatorClass().equals(segment.getInterpolator().getClass())) { + try { + defaultInterpolator = mod.cloneInterpolator(segment.getInterpolator()); + break; + } catch (IOException e) { + Utils.error(LOGGER, replayHandler.getOverlay(), + CrashReport.makeCrashReport(e, "Cloning fixed interpolator"), null); + } + } + } + + // if no segment with the default interpolator was found, + // create a new one with default settings + if (defaultInterpolator == null) { + switch (type) { + case LINEAR: + defaultInterpolator = new LinearInterpolator(); + break; + case CUBIC: + default: + defaultInterpolator = new CubicSplineInterpolator(); + break; + } + } + List changes = new ArrayList<>(); Interpolator interpolator = null; boolean isSpectatorInterpolator = false; - for (PathSegment segment : mod.getCurrentTimeline().getPaths().get(POSITION_PATH).getSegments()) { + for (PathSegment segment : pathSegments) { if (segment.getStartKeyframe().getValue(SpectatorProperty.PROPERTY).isPresent() && segment.getEndKeyframe().getValue(SpectatorProperty.PROPERTY).isPresent()) { // Spectator segment @@ -653,11 +690,23 @@ public class GuiPathing { changes.add(SetInterpolator.create(segment, interpolator)); } else { // Normal segment - if (isSpectatorInterpolator || interpolator == null) { + boolean explicit = segment.getStartKeyframe().getValue(ExplicitInterpolationProperty.PROPERTY).isPresent(); + if (isSpectatorInterpolator || interpolator == null || explicit) { isSpectatorInterpolator = false; - interpolator = linearInterpolation ? new LinearInterpolator() : new CubicSplineInterpolator(); - interpolator.registerProperty(CameraProperties.POSITION); - interpolator.registerProperty(CameraProperties.ROTATION); + + if (segment.getInterpolator() == null || !explicit) { + interpolator = defaultInterpolator; + interpolator.registerProperty(CameraProperties.POSITION); + interpolator.registerProperty(CameraProperties.ROTATION); + } else { + try { + interpolator = mod.cloneInterpolator(segment.getInterpolator()); + } catch (IOException e) { + Utils.error(LOGGER, replayHandler.getOverlay(), + CrashReport.makeCrashReport(e, "Cloning fixed interpolator"), null); + interpolator = defaultInterpolator; + } + } } changes.add(SetInterpolator.create(segment, interpolator)); } @@ -697,10 +746,18 @@ public class GuiPathing { public Change moveKeyframe(Path path, Keyframe keyframe, long newTime) { Timeline timeline = mod.getCurrentTimeline(); - // Interpolator might be required later (only if path is the time path) - Optional interpolator = + // InterpolatorType might be required later (only if path is the time path) + Optional firstInterpolator = path.getSegments().stream().findFirst().map(PathSegment::getInterpolator); + // The interpolator before the segment + Optional interpolatorBefore = + path.getSegments().stream().filter(s -> s.getEndKeyframe() == keyframe).findFirst().map(PathSegment::getInterpolator); + + // The interpolator that follows the segment + Optional interpolatorAfter = + path.getSegments().stream().filter(s -> s.getStartKeyframe() == keyframe).findFirst().map(PathSegment::getInterpolator); + // First remove the old keyframe Change removeChange = RemoveKeyframe.create(path, keyframe); removeChange.apply(timeline); @@ -708,7 +765,7 @@ public class GuiPathing { // and add a new one at the correct time Change addChange = AddKeyframe.create(path, newTime); addChange.apply(timeline); - path.getKeyframe(newTime); + Keyframe newKeyframe = path.getKeyframe(newTime); // Then copy over all properties UpdateKeyframeProperties.Builder builder = UpdateKeyframeProperties.create(path, path.getKeyframe(newTime)); @@ -718,25 +775,51 @@ public class GuiPathing { Change propertyChange = builder.done(); propertyChange.apply(timeline); - // Finally set the interpolators - Change interpolatorChange; + // Set the interpolator of the segment before the keyframe to what it was + Change interpolatorBeforeChange; + + Optional segmentBefore = path.getSegments().stream().filter(s -> s.getEndKeyframe() == newKeyframe).findFirst(); + if (segmentBefore.isPresent() && interpolatorBefore.isPresent()) { + interpolatorBeforeChange = SetInterpolator.create(segmentBefore.get(), interpolatorBefore.get()); + } else { + interpolatorBeforeChange = CombinedChange.create(); + } + + interpolatorBeforeChange.apply(timeline); + + // Set the interpolator of the segment after the keyframe to what it was + Change interpolatorAfterChange; + + Optional segmentAfter = path.getSegments().stream().filter(s -> s.getStartKeyframe() == newKeyframe).findFirst(); + if (segmentAfter.isPresent() && interpolatorAfter.isPresent()) { + interpolatorAfterChange = SetInterpolator.create(segmentAfter.get(), interpolatorAfter.get()); + } else { + interpolatorAfterChange = CombinedChange.create(); + } + + interpolatorAfterChange.apply(timeline); + + // Finally update the interpolators + Change interpolatorUpdateChange; if (path.getTimeline().getPaths().indexOf(path) == GuiPathing.POSITION_PATH) { // Position / Spectator keyframes need special handling - interpolatorChange = updateInterpolators(); + interpolatorUpdateChange = updateInterpolators(); } else { // Time keyframes only need updating when only one segment of them exists if (path.getSegments().size() == 1) { - interpolatorChange = SetInterpolator.create(path.getSegments().iterator().next(), interpolator.get()); + interpolatorUpdateChange = SetInterpolator.create(path.getSegments().iterator().next(), firstInterpolator.get()); } else { - interpolatorChange = CombinedChange.create(); // Noop change + interpolatorUpdateChange = CombinedChange.create(); // Noop change } } - interpolatorChange.apply(timeline); + + interpolatorUpdateChange.apply(timeline); // and update spectator positions Change spectatorChange = updateSpectatorPositions(); spectatorChange.apply(timeline); - return CombinedChange.createFromApplied(removeChange, addChange, propertyChange, interpolatorChange, spectatorChange); + return CombinedChange.createFromApplied(removeChange, addChange, propertyChange, + interpolatorBeforeChange, interpolatorAfterChange, interpolatorUpdateChange, spectatorChange); } // Helper method because generics cannot be defined on blocks diff --git a/src/main/resources/assets/replaymod/lang/en_US.lang b/src/main/resources/assets/replaymod/lang/en_US.lang index 3c85d8d8..e68f9897 100644 --- a/src/main/resources/assets/replaymod/lang/en_US.lang +++ b/src/main/resources/assets/replaymod/lang/en_US.lang @@ -309,6 +309,7 @@ replaymod.gui.settings.keyframecleancallback=Clear Confirmation replaymod.gui.settings.renderinvisible=Render invisible Entities replaymod.gui.settings.camera=Camera replaymod.gui.settings.showchat=Show Chat +replaymod.gui.settings.interpolator=Default Interpolator replaymod.gui.settings.warning.linea=WARNING: Recording settings will be replaymod.gui.settings.warning.lineb=applied the next time you join a world. @@ -372,6 +373,14 @@ replaymod.gui.editkeyframe.spec.method.shoulder.pitch=Pitch Offset replaymod.gui.editkeyframe.spec.method.shoulder.yaw=Rotation Angle replaymod.gui.editkeyframe.spec.method.shoulder.smoothness=Path Smoothness +replaymod.gui.editkeyframe.interpolator=Interpolator +replaymod.gui.editkeyframe.interpolator.default.name=Default Interpolator +replaymod.gui.editkeyframe.interpolator.default.desc=Uses the default interpolator defined in the Replay Mod Settings. +replaymod.gui.editkeyframe.interpolator.cubic.name=Cubic Spline Interpolator +replaymod.gui.editkeyframe.interpolator.cubic.desc=Calculates a cubic equation matrix for all points for a smooth camera path. +replaymod.gui.editkeyframe.interpolator.linear.name=Linear Interpolator +replaymod.gui.editkeyframe.interpolator.linear.desc=Draws straight lines between the keyframes. + #Render Settings GUI replaymod.gui.rendersettings.title=Rendering Options replaymod.gui.rendersettings.renderer=Rendering Method From 057edccf26350a0c4b39faa309708a8a1799af14 Mon Sep 17 00:00:00 2001 From: johni0702 Date: Thu, 19 Jan 2017 17:06:29 +0100 Subject: [PATCH 02/19] Manually inject root cert for cross-signed LetsEncrypt cert in requests JREs prior to 8u101 do not ship with the root certificate that cross-signed the LetsEncrypt X3 CA used to sign server certificates. As the standard native Minecraft launcher for Windows and OSX ship with an older Java version, this commit imports the required root cert and uses it wherever communication with the replaymod.com server is established. Effectively reverts 682fb4b --- .../java/com/replaymod/core/utils/Utils.java | 48 +++++++++++++++++++ .../com/replaymod/extras/OpenEyeExtra.java | 9 +++- .../com/replaymod/online/api/ApiClient.java | 19 ++++++-- .../replaymod/online/api/SimpleApiClient.java | 9 ++-- .../online/api/replay/FileUploader.java | 7 ++- .../api/replay/ReplayModApiMethods.java | 2 +- src/main/resources/dst_root_ca_x3.pem | 20 ++++++++ 7 files changed, 102 insertions(+), 12 deletions(-) create mode 100644 src/main/resources/dst_root_ca_x3.pem diff --git a/src/main/java/com/replaymod/core/utils/Utils.java b/src/main/java/com/replaymod/core/utils/Utils.java index 2457ae15..8b27fff4 100644 --- a/src/main/java/com/replaymod/core/utils/Utils.java +++ b/src/main/java/com/replaymod/core/utils/Utils.java @@ -27,8 +27,20 @@ import org.lwjgl.util.ReadableDimension; import javax.annotation.Nullable; import javax.imageio.ImageIO; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManagerFactory; import java.awt.image.BufferedImage; import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Date; @@ -51,6 +63,42 @@ public class Utils { DEFAULT_THUMBNAIL = thumbnail; } + + /** + * Neither the root certificate of LetsEncrypt nor the root that cross-signed it is included in the default + * Java keystore prior to 8u101. + * Therefore whenever a connection to the replaymod.com site is made, this SSLContext has to be used instead. + * It has been constructed to include the necessary root certificates. + * @see #SSL_SOCKET_FACTORY + */ + public static final SSLContext SSL_CONTEXT; + + /** + * @see #SSL_CONTEXT + */ + public static final SSLSocketFactory SSL_SOCKET_FACTORY; + + static { + // Largely from https://community.letsencrypt.org/t/134/37 + try (InputStream in = Utils.class.getResourceAsStream("/dst_root_ca_x3.pem")){ + Certificate certificate = CertificateFactory.getInstance("X.509").generateCertificate(in); + + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load(null, null); + keyStore.setCertificateEntry("1", certificate); + + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(keyStore); + + SSLContext ctx = SSLContext.getInstance("TLS"); + ctx.init(null, trustManagerFactory.getTrustManagers(), null); + SSL_CONTEXT = ctx; + SSL_SOCKET_FACTORY = ctx.getSocketFactory(); + } catch (IOException | CertificateException | KeyStoreException | NoSuchAlgorithmException | KeyManagementException e) { + throw new RuntimeException(e); + } + } + public static String convertSecondsToShortString(int seconds) { int hours = seconds/(60*60); int min = seconds/60 - hours*60; diff --git a/src/main/java/com/replaymod/extras/OpenEyeExtra.java b/src/main/java/com/replaymod/extras/OpenEyeExtra.java index e198a826..ad5c1004 100644 --- a/src/main/java/com/replaymod/extras/OpenEyeExtra.java +++ b/src/main/java/com/replaymod/extras/OpenEyeExtra.java @@ -17,6 +17,7 @@ import de.johni0702.minecraft.gui.utils.Colors; import net.minecraftforge.fml.common.Loader; import org.apache.commons.io.FileUtils; +import javax.net.ssl.HttpsURLConnection; import java.io.File; import java.io.FileOutputStream; import java.net.URL; @@ -24,8 +25,10 @@ import java.nio.channels.Channels; import java.nio.channels.FileChannel; import java.nio.channels.ReadableByteChannel; +import static com.replaymod.core.utils.Utils.SSL_SOCKET_FACTORY; + public class OpenEyeExtra implements Extra { - private static final String DOWNLOAD_URL = "http://www.replaymod.com/dl/openeye/" + Loader.MC_VERSION; + private static final String DOWNLOAD_URL = "https://www.replaymod.com/dl/openeye/" + Loader.MC_VERSION; private static final Setting ASK_FOR_OPEN_EYE = new Setting<>("advanced", "askForOpenEye", null, true); private ReplayMod mod; @@ -69,7 +72,9 @@ public class OpenEyeExtra implements Extra { File targetFile = new File("mods/" + Loader.MC_VERSION, "OpenEye.jar"); FileUtils.forceMkdir(targetFile.getParentFile()); - ReadableByteChannel in = Channels.newChannel(new URL(DOWNLOAD_URL).openStream()); + HttpsURLConnection connection = (HttpsURLConnection) new URL(DOWNLOAD_URL).openConnection(); + connection.setSSLSocketFactory(SSL_SOCKET_FACTORY); + ReadableByteChannel in = Channels.newChannel(connection.getInputStream()); FileChannel out = new FileOutputStream(targetFile).getChannel(); out.transferFrom(in, 0, Long.MAX_VALUE); } catch (Throwable e) { diff --git a/src/main/java/com/replaymod/online/api/ApiClient.java b/src/main/java/com/replaymod/online/api/ApiClient.java index f8e8a1b1..a12cd822 100755 --- a/src/main/java/com/replaymod/online/api/ApiClient.java +++ b/src/main/java/com/replaymod/online/api/ApiClient.java @@ -15,13 +15,19 @@ import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import javax.imageio.ImageIO; +import javax.net.ssl.HttpsURLConnection; import java.awt.image.BufferedImage; -import java.io.*; -import java.net.HttpURLConnection; +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; import java.net.URL; import java.util.List; import java.util.function.Consumer; +import static com.replaymod.core.utils.Utils.SSL_SOCKET_FACTORY; + public class ApiClient { private static final Minecraft mc = Minecraft.getMinecraft(); @@ -130,7 +136,11 @@ public class ApiClient { QueryBuilder builder = new QueryBuilder(ReplayModApiMethods.get_thumbnail); builder.put("id", file); URL url = new URL(builder.toString()); - return ImageIO.read(url); + HttpsURLConnection connection = (HttpsURLConnection) url.openConnection(); + connection.setSSLSocketFactory(SSL_SOCKET_FACTORY); + try (InputStream in = connection.getInputStream()) { + return ImageIO.read(in); + } } private boolean cancelDownload = false; @@ -143,7 +153,8 @@ public class ApiClient { builder.put("id", file); String url = builder.toString(); URL website = new URL(url); - HttpURLConnection con = (HttpURLConnection) website.openConnection(); + HttpsURLConnection con = (HttpsURLConnection) website.openConnection(); + con.setSSLSocketFactory(SSL_SOCKET_FACTORY); int fileSize = con.getContentLength(); diff --git a/src/main/java/com/replaymod/online/api/SimpleApiClient.java b/src/main/java/com/replaymod/online/api/SimpleApiClient.java index b2871c6c..c3add7a3 100755 --- a/src/main/java/com/replaymod/online/api/SimpleApiClient.java +++ b/src/main/java/com/replaymod/online/api/SimpleApiClient.java @@ -7,12 +7,14 @@ import com.google.gson.JsonParser; import com.replaymod.online.api.replay.holders.ApiError; import org.apache.commons.io.IOUtils; +import javax.net.ssl.HttpsURLConnection; import java.io.IOException; import java.io.InputStream; -import java.net.HttpURLConnection; import java.net.URL; import java.util.Map; +import static com.replaymod.core.utils.Utils.SSL_SOCKET_FACTORY; + public class SimpleApiClient { private static final JsonParser jsonParser = new JsonParser(); @@ -72,10 +74,11 @@ public class SimpleApiClient { // read response String responseContent = null; InputStream is = null; - HttpURLConnection httpUrlConnection = null; + HttpsURLConnection httpUrlConnection = null; try { URL url = new URL(urlString); - httpUrlConnection = (HttpURLConnection) url.openConnection(); + httpUrlConnection = (HttpsURLConnection) url.openConnection(); + httpUrlConnection.setSSLSocketFactory(SSL_SOCKET_FACTORY); httpUrlConnection.setRequestMethod("GET"); diff --git a/src/main/java/com/replaymod/online/api/replay/FileUploader.java b/src/main/java/com/replaymod/online/api/replay/FileUploader.java index f369b310..8dce3faa 100755 --- a/src/main/java/com/replaymod/online/api/replay/FileUploader.java +++ b/src/main/java/com/replaymod/online/api/replay/FileUploader.java @@ -9,16 +9,18 @@ import lombok.RequiredArgsConstructor; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; +import javax.net.ssl.HttpsURLConnection; import java.io.DataOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; -import java.net.HttpURLConnection; import java.net.URL; import java.net.URLEncoder; import java.util.Set; import java.util.function.Consumer; +import static com.replaymod.core.utils.Utils.SSL_SOCKET_FACTORY; + @RequiredArgsConstructor public class FileUploader { private static final Gson gson = new Gson(); @@ -48,7 +50,8 @@ public class FileUploader { postData += "&name=" + URLEncoder.encode(filename, "UTF-8"); String url = ReplayModApiMethods.upload_file + postData; - HttpURLConnection con = (HttpURLConnection) new URL(url).openConnection(); + HttpsURLConnection con = (HttpsURLConnection) new URL(url).openConnection(); + con.setSSLSocketFactory(SSL_SOCKET_FACTORY); con.setUseCaches(false); con.setDoOutput(true); con.setRequestMethod("POST"); diff --git a/src/main/java/com/replaymod/online/api/replay/ReplayModApiMethods.java b/src/main/java/com/replaymod/online/api/replay/ReplayModApiMethods.java index 6277c1cd..524fbba1 100755 --- a/src/main/java/com/replaymod/online/api/replay/ReplayModApiMethods.java +++ b/src/main/java/com/replaymod/online/api/replay/ReplayModApiMethods.java @@ -2,7 +2,7 @@ package com.replaymod.online.api.replay; public class ReplayModApiMethods { - public static final String REPLAYMOD_BASE_URL = "http://ReplayMod.com/api/"; + public static final String REPLAYMOD_BASE_URL = "https://ReplayMod.com/api/"; public static final String register = REPLAYMOD_BASE_URL+"register"; public static final String check_authkey = REPLAYMOD_BASE_URL+"check_authkey"; diff --git a/src/main/resources/dst_root_ca_x3.pem b/src/main/resources/dst_root_ca_x3.pem new file mode 100644 index 00000000..b2e43c93 --- /dev/null +++ b/src/main/resources/dst_root_ca_x3.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/ +MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT +DkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow +PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD +Ew5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +AN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O +rz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq +OLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b +xiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw +7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD +aeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV +HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG +SIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69 +ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr +AvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz +R8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5 +JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo +Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ +-----END CERTIFICATE----- From 5302bcaed4cc2052506e30beab54e06f0d4bef14 Mon Sep 17 00:00:00 2001 From: johni0702 Date: Fri, 20 Jan 2017 16:02:42 +0100 Subject: [PATCH 03/19] Fix id and text of Replay Editor button in docs --- docs/content.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content.md b/docs/content.md index 0b3f97fe..90f9a4f5 100644 --- a/docs/content.md +++ b/docs/content.md @@ -56,7 +56,7 @@ Please use [Optifine](https://optifine.net/) instead. ### Custom Main Menu [custom-main-menu] The [Custom Main Menu](https://mods.curse.com/mc-mods/minecraft/226406-custom-main-menu) mod is often used in mod packs to customize their Main Menu with a button layout fitting the background image, links to their website / bug tracker and similar. -If you are familiar with it, the button ids for the Replay Mod are: **17890234** (text: `replaymod.gui.replayviewer`), **17890235** (text: `replaymod.gui.replayviewer`) and **17890236** (text: `replaymod.gui.replaycenter`) +If you are familiar with it, the button ids for the Replay Mod are: **17890234** (text: `replaymod.gui.replayviewer`), **17890237** (text: `replaymod.gui.replayeditor`) and **17890236** (text: `replaymod.gui.replaycenter`) Due to the nature of this Custom Main Menu mod, buttons added to the Main Menu by 3rd party mods like the **Replay Mod** will not show up by default. Thus, to access the Replay Viewer/Editor/Center, you need to manually configure the position for those buttons. From 4deb374a7f6e6cdc78a46f163dfb39dcb5e8768b Mon Sep 17 00:00:00 2001 From: johni0702 Date: Fri, 20 Jan 2017 16:16:01 +0100 Subject: [PATCH 04/19] Enable validation on focus change for all number input fields --- .../replaymod/editor/gui/GuiTrimPanel.java | 20 +++++++++++-------- .../render/gui/GuiRenderSettings.java | 6 +++--- .../gui/overlay/GuiEditMarkerPopup.java | 18 ++++++++++------- 3 files changed, 26 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/replaymod/editor/gui/GuiTrimPanel.java b/src/main/java/com/replaymod/editor/gui/GuiTrimPanel.java index ae5fd79e..fda001f1 100644 --- a/src/main/java/com/replaymod/editor/gui/GuiTrimPanel.java +++ b/src/main/java/com/replaymod/editor/gui/GuiTrimPanel.java @@ -39,6 +39,10 @@ import static com.replaymod.editor.ReplayModEditor.LOGGER; import static java.util.Optional.ofNullable; public class GuiTrimPanel extends GuiPanel { + private static GuiNumberField newGuiNumberField() { + return new GuiNumberField().setMaxLength(2).setSize(20, 20).setValidateOnFocusChange(true); + } + // Special value indicating no replay files were found private static final File NO_REPLAY = new File("."); // Special value for the initial "Select Marker Keyframe" entry @@ -50,16 +54,16 @@ public class GuiTrimPanel extends GuiPanel { .setMinSize(new Dimension(200, 20)).onSelection(i -> updateSelectedReplay()) .setToString(f -> f == NO_REPLAY ? "" : FilenameUtils.getBaseName(f.getName())); - public final GuiNumberField startHour = new GuiNumberField().setSize(20, 20).setMaxLength(2); - public final GuiNumberField startMin = new GuiNumberField().setSize(20, 20).setMaxLength(2); - public final GuiNumberField startSec = new GuiNumberField().setSize(20, 20).setMaxLength(2); - public final GuiNumberField startMilli = new GuiNumberField().setSize(40, 20).setMaxLength(4); + public final GuiNumberField startHour = newGuiNumberField(); + public final GuiNumberField startMin = newGuiNumberField(); + public final GuiNumberField startSec = newGuiNumberField(); + public final GuiNumberField startMilli = newGuiNumberField().setSize(40, 20).setMaxLength(4); public final GuiDropdownMenu startMarker = new GuiDropdownMenu<>(); - public final GuiNumberField endHour = new GuiNumberField().setSize(20, 20).setMaxLength(2); - public final GuiNumberField endMin = new GuiNumberField().setSize(20, 20).setMaxLength(2); - public final GuiNumberField endSec = new GuiNumberField().setSize(20, 20).setMaxLength(2); - public final GuiNumberField endMilli = new GuiNumberField().setSize(40, 20).setMaxLength(4); + public final GuiNumberField endHour = newGuiNumberField(); + public final GuiNumberField endMin = newGuiNumberField(); + public final GuiNumberField endSec = newGuiNumberField(); + public final GuiNumberField endMilli = newGuiNumberField().setSize(40, 20).setMaxLength(4); public final GuiDropdownMenu endMarker = new GuiDropdownMenu<>(); public final GuiPanel timePanel = new GuiPanel(this) diff --git a/src/main/java/com/replaymod/render/gui/GuiRenderSettings.java b/src/main/java/com/replaymod/render/gui/GuiRenderSettings.java index 14cd84fc..3c2faa88 100644 --- a/src/main/java/com/replaymod/render/gui/GuiRenderSettings.java +++ b/src/main/java/com/replaymod/render/gui/GuiRenderSettings.java @@ -80,8 +80,8 @@ public class GuiRenderSettings extends GuiScreen implements Closeable { } }).setMinSize(new Dimension(0, 20)).setValues(RenderSettings.EncodingPreset.values()); - public final GuiNumberField videoWidth = new GuiNumberField().setSize(50, 20).setMinValue(1); - public final GuiNumberField videoHeight = new GuiNumberField().setSize(50, 20).setMinValue(1); + public final GuiNumberField videoWidth = new GuiNumberField().setSize(50, 20).setMinValue(1).setValidateOnFocusChange(true); + public final GuiNumberField videoHeight = new GuiNumberField().setSize(50, 20).setMinValue(1).setValidateOnFocusChange(true); public final GuiSlider frameRateSlider = new GuiSlider().onValueChanged(new Runnable() { @Override public void run() { @@ -93,7 +93,7 @@ public class GuiRenderSettings extends GuiScreen implements Closeable { .setLayout(new HorizontalLayout(HorizontalLayout.Alignment.RIGHT).setSpacing(2)) .addElements(new HorizontalLayout.Data(0.5), videoWidth, new GuiLabel().setText("*"), videoHeight); - public final GuiNumberField bitRateField = new GuiNumberField().setValue(10).setSize(50, 20); + public final GuiNumberField bitRateField = new GuiNumberField().setValue(10).setSize(50, 20).setValidateOnFocusChange(true); public final GuiDropdownMenu bitRateUnit = new GuiDropdownMenu() .setSize(50, 20).setValues("bps", "kbps", "mbps").setSelected("mbps"); 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 23e9e5b5..cc94f3cc 100644 --- a/src/main/java/com/replaymod/replay/gui/overlay/GuiEditMarkerPopup.java +++ b/src/main/java/com/replaymod/replay/gui/overlay/GuiEditMarkerPopup.java @@ -16,6 +16,10 @@ import org.lwjgl.input.Keyboard; import org.lwjgl.util.ReadablePoint; public class GuiEditMarkerPopup extends AbstractGuiPopup implements Typeable { + private static GuiNumberField newGuiNumberField() { + return new GuiNumberField().setSize(150, 20).setValidateOnFocusChange(true); + } + private final ReplayHandler replayHandler; private final Marker marker; @@ -23,15 +27,15 @@ public class GuiEditMarkerPopup extends AbstractGuiPopup imp public final GuiTextField nameField = new GuiTextField().setSize(150, 20); // TODO: Replace with a min/sec/msec field - public final GuiNumberField timeField = new GuiNumberField().setSize(150, 20).setPrecision(0); + public final GuiNumberField timeField = newGuiNumberField().setPrecision(0); - public final GuiNumberField xField = new GuiNumberField().setSize(150, 20).setPrecision(10); - public final GuiNumberField yField = new GuiNumberField().setSize(150, 20).setPrecision(10); - public final GuiNumberField zField = new GuiNumberField().setSize(150, 20).setPrecision(10); + public final GuiNumberField xField = newGuiNumberField().setPrecision(10); + public final GuiNumberField yField = newGuiNumberField().setPrecision(10); + public final GuiNumberField zField = newGuiNumberField().setPrecision(10); - public final GuiNumberField yawField = new GuiNumberField().setSize(150, 20).setPrecision(5); - public final GuiNumberField pitchField = new GuiNumberField().setSize(150, 20).setPrecision(5); - public final GuiNumberField rollField = new GuiNumberField().setSize(150, 20).setPrecision(5); + public final GuiNumberField yawField = newGuiNumberField().setPrecision(5); + public final GuiNumberField pitchField = newGuiNumberField().setPrecision(5); + public final GuiNumberField rollField = newGuiNumberField().setPrecision(5); public final GuiPanel inputs = GuiPanel.builder() .layout(new GridLayout().setColumns(2).setSpacingX(7).setSpacingY(3)) From 2c640304594dafa0106c10dab14511d6f4344fcb Mon Sep 17 00:00:00 2001 From: johni0702 Date: Sat, 21 Jan 2017 13:28:14 +0100 Subject: [PATCH 05/19] Refactor simple pathing timeline logic into dedicated class with unit tests Fix logic for user-set interpolators --- .../simplepathing/InterpolatorType.java | 51 ++ .../simplepathing/ReplayModSimplePathing.java | 132 +--- .../replaymod/simplepathing/SPTimeline.java | 623 ++++++++++++++++++ .../simplepathing/gui/GuiEditKeyframe.java | 229 +++---- .../gui/GuiKeyframeTimeline.java | 88 ++- .../simplepathing/gui/GuiPathing.java | 428 ++++-------- .../preview/PathPreviewRenderer.java | 10 +- .../ExplicitInterpolationProperty.java | 7 +- .../simplepathing/SPTimelineTest.java | 594 +++++++++++++++++ 9 files changed, 1546 insertions(+), 616 deletions(-) create mode 100644 src/main/java/com/replaymod/simplepathing/InterpolatorType.java create mode 100644 src/main/java/com/replaymod/simplepathing/SPTimeline.java rename src/main/java/com/replaymod/{pathing => simplepathing}/properties/ExplicitInterpolationProperty.java (85%) create mode 100644 src/test/java/com/replaymod/simplepathing/SPTimelineTest.java diff --git a/src/main/java/com/replaymod/simplepathing/InterpolatorType.java b/src/main/java/com/replaymod/simplepathing/InterpolatorType.java new file mode 100644 index 00000000..1c02f201 --- /dev/null +++ b/src/main/java/com/replaymod/simplepathing/InterpolatorType.java @@ -0,0 +1,51 @@ +package com.replaymod.simplepathing; + +import com.replaymod.replaystudio.pathing.interpolation.CubicSplineInterpolator; +import com.replaymod.replaystudio.pathing.interpolation.Interpolator; +import com.replaymod.replaystudio.pathing.interpolation.LinearInterpolator; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.function.Supplier; + +@AllArgsConstructor +public enum InterpolatorType { + DEFAULT("default", null, null), + CUBIC("cubic", CubicSplineInterpolator.class, CubicSplineInterpolator::new), + LINEAR("linear", LinearInterpolator.class, LinearInterpolator::new); + + private String localizationKey; + + @Getter + private Class interpolatorClass; + + private Supplier interpolatorConstructor; + + public String getI18nName() { + return String.format("replaymod.gui.editkeyframe.interpolator.%1$s.name", localizationKey); + } + + public String getI18nDescription() { + return String.format("replaymod.gui.editkeyframe.interpolator.%1$s.desc", localizationKey); + } + + public static InterpolatorType fromString(String string) { + for (InterpolatorType t : values()) { + if (t.getI18nName().equals(string)) return t; + } + return CUBIC; //the default + } + + public static InterpolatorType fromClass(Class cls) { + for (InterpolatorType type : values()) { + if (cls.equals(type.getInterpolatorClass())) { + return type; + } + } + return DEFAULT; + } + + public Interpolator newInstance() { + return interpolatorConstructor.get(); + } +} diff --git a/src/main/java/com/replaymod/simplepathing/ReplayModSimplePathing.java b/src/main/java/com/replaymod/simplepathing/ReplayModSimplePathing.java index d55ae73c..85dbd5d9 100644 --- a/src/main/java/com/replaymod/simplepathing/ReplayModSimplePathing.java +++ b/src/main/java/com/replaymod/simplepathing/ReplayModSimplePathing.java @@ -1,41 +1,25 @@ package com.replaymod.simplepathing; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonWriter; import com.replaymod.core.ReplayMod; import com.replaymod.core.events.SettingsChangedEvent; -import com.replaymod.pathing.properties.CameraProperties; -import com.replaymod.pathing.properties.ExplicitInterpolationProperty; -import com.replaymod.pathing.properties.SpectatorProperty; -import com.replaymod.pathing.properties.TimestampProperty; import com.replaymod.replay.events.ReplayCloseEvent; import com.replaymod.replay.events.ReplayOpenEvent; -import com.replaymod.replaystudio.pathing.PathingRegistry; -import com.replaymod.replaystudio.pathing.impl.TimelineImpl; -import com.replaymod.replaystudio.pathing.interpolation.CubicSplineInterpolator; -import com.replaymod.replaystudio.pathing.interpolation.Interpolator; -import com.replaymod.replaystudio.pathing.interpolation.LinearInterpolator; import com.replaymod.replaystudio.pathing.path.Keyframe; -import com.replaymod.replaystudio.pathing.path.Timeline; -import com.replaymod.replaystudio.pathing.property.Property; +import com.replaymod.simplepathing.SPTimeline.SPPath; import com.replaymod.simplepathing.gui.GuiPathing; import com.replaymod.simplepathing.preview.PathPreview; +import lombok.Getter; import net.minecraftforge.fml.common.FMLCommonHandler; import net.minecraftforge.fml.common.Mod; import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; import org.apache.logging.log4j.Logger; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.PrintWriter; -import java.io.StringReader; - @Mod(modid = ReplayModSimplePathing.MOD_ID, version = "@MOD_VERSION@", acceptedMinecraftVersions = "@MC_VERSION@", useMetadata = true) -public class ReplayModSimplePathing implements PathingRegistry { +public class ReplayModSimplePathing { public static final String MOD_ID = "replaymod-simplepathing"; private ReplayMod core; @@ -59,114 +43,60 @@ public class ReplayModSimplePathing implements PathingRegistry { @SubscribeEvent public void postReplayOpen(ReplayOpenEvent.Post event) { + currentTimeline = new SPTimeline(); guiPathing = new GuiPathing(core, this, event.getReplayHandler()); } @SubscribeEvent public void onReplayClose(ReplayCloseEvent.Post event) { + currentTimeline = null; guiPathing = null; - currentTimeline = createTimeline(); - currentTimeline.createPath(); - currentTimeline.createPath(); - selectedKeyframe = null; + selectedPath = null; } @SubscribeEvent public void onSettingsChanged(SettingsChangedEvent event) { if (event.getKey() == Setting.DEFAULT_INTERPOLATION) { if (currentTimeline != null && guiPathing != null) { - currentTimeline.applyChange(guiPathing.updateInterpolators()); + updateDefaultInterpolatorType(); } } } - private Timeline currentTimeline = createTimeline(); { currentTimeline.createPath(); currentTimeline.createPath(); } - private Keyframe selectedKeyframe; + private SPTimeline currentTimeline; - public Keyframe getSelectedKeyframe() { - return selectedKeyframe; + @Getter + private SPPath selectedPath; + @Getter + private long selectedTime; + + public boolean isSelected(Keyframe keyframe) { + return selectedPath != null && currentTimeline.getKeyframe(selectedPath, selectedTime) == keyframe; } - public void setSelectedKeyframe(Keyframe selected) { - this.selectedKeyframe = selected; + public void setSelected(SPPath path, long time) { + selectedPath = path; + selectedTime = time; } - public void setCurrentTimeline(Timeline currentTimeline) { - if (this.currentTimeline != currentTimeline) { - selectedKeyframe = null; - } - this.currentTimeline = currentTimeline; + public void setCurrentTimeline(SPTimeline newTimeline) { + selectedPath = null; + currentTimeline = newTimeline; } - public Timeline getCurrentTimeline() { + public void clearCurrentTimeline() { + setCurrentTimeline(new SPTimeline()); + updateDefaultInterpolatorType(); + } + + public SPTimeline getCurrentTimeline() { return currentTimeline; } - @Override - public Timeline createTimeline() { - Timeline timeline = new TimelineImpl(); - - timeline.registerProperty(TimestampProperty.PROPERTY); - timeline.registerProperty(CameraProperties.POSITION); - timeline.registerProperty(CameraProperties.ROTATION); - timeline.registerProperty(SpectatorProperty.PROPERTY); - timeline.registerProperty(ExplicitInterpolationProperty.PROPERTY); - - return timeline; - } - - @Override - public void serializeInterpolator(JsonWriter writer, Interpolator interpolator) throws IOException { - if (interpolator instanceof LinearInterpolator) { - writer.value("linear"); - } else if (interpolator instanceof CubicSplineInterpolator) { - writer.value("cubic-spline"); - } else { - throw new IOException("Unknown interpolator type: " + interpolator); - } - } - - @Override - public Interpolator deserializeInterpolator(JsonReader reader) throws IOException { - String type = reader.nextString(); - switch (type) { - case "linear": - return new LinearInterpolator(); - case "cubic-spline": - return new CubicSplineInterpolator(); - default: - throw new IOException("Unknown interpolation type: " + type); - - } - } - - /** - * Clones an interpolator by de- and reserializing it. - * @param interpolator The interpolator to clone - * @return The cloned interpolator - * @throws IOException - */ - public Interpolator cloneInterpolator(Interpolator interpolator) throws IOException { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - PrintWriter printWriter = new PrintWriter(baos); - - JsonWriter jsonWriter = new JsonWriter(printWriter); - jsonWriter.beginArray(); - serializeInterpolator(jsonWriter, interpolator); - jsonWriter.endArray(); - jsonWriter.flush(); - - String json = baos.toString(); - - JsonReader jsonReader = new JsonReader(new StringReader(json)); - jsonReader.beginArray(); - Interpolator cloned = deserializeInterpolator(jsonReader); - - for (Property p : interpolator.getKeyframeProperties()) { - cloned.registerProperty(p); - } - - return cloned; + private void updateDefaultInterpolatorType() { + InterpolatorType newDefaultType = + InterpolatorType.fromString(core.getSettingsRegistry().get(Setting.DEFAULT_INTERPOLATION)); + currentTimeline.setDefaultInterpolatorType(newDefaultType); } public ReplayMod getCore() { diff --git a/src/main/java/com/replaymod/simplepathing/SPTimeline.java b/src/main/java/com/replaymod/simplepathing/SPTimeline.java new file mode 100644 index 00000000..26442b66 --- /dev/null +++ b/src/main/java/com/replaymod/simplepathing/SPTimeline.java @@ -0,0 +1,623 @@ +package com.replaymod.simplepathing; + +import com.google.common.base.Preconditions; +import com.google.common.collect.Iterables; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import com.replaymod.pathing.properties.CameraProperties; +import com.replaymod.simplepathing.properties.ExplicitInterpolationProperty; +import com.replaymod.pathing.properties.SpectatorProperty; +import com.replaymod.pathing.properties.TimestampProperty; +import com.replaymod.replaystudio.pathing.PathingRegistry; +import com.replaymod.replaystudio.pathing.change.AddKeyframe; +import com.replaymod.replaystudio.pathing.change.Change; +import com.replaymod.replaystudio.pathing.change.CombinedChange; +import com.replaymod.replaystudio.pathing.change.SetInterpolator; +import com.replaymod.replaystudio.pathing.change.UpdateKeyframeProperties; +import com.replaymod.replaystudio.pathing.impl.TimelineImpl; +import com.replaymod.replaystudio.pathing.interpolation.CubicSplineInterpolator; +import com.replaymod.replaystudio.pathing.interpolation.Interpolator; +import com.replaymod.replaystudio.pathing.interpolation.LinearInterpolator; +import com.replaymod.replaystudio.pathing.path.Keyframe; +import com.replaymod.replaystudio.pathing.path.Path; +import com.replaymod.replaystudio.pathing.path.PathSegment; +import com.replaymod.replaystudio.pathing.path.Timeline; +import com.replaymod.replaystudio.pathing.property.Property; +import com.replaymod.replaystudio.util.EntityPositionTracker; +import com.replaymod.replaystudio.util.Location; +import lombok.Getter; +import net.minecraft.crash.CrashReport; +import net.minecraft.crash.CrashReportCategory; +import net.minecraft.util.ReportedException; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.tuple.Triple; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import static com.replaymod.replaystudio.pathing.change.RemoveKeyframe.create; +import static com.replaymod.simplepathing.ReplayModSimplePathing.LOGGER; + +/** + * Simplified timeline abstraction used in the SimplePathing module. + */ +public class SPTimeline implements PathingRegistry { + public enum SPPath { + TIME, + POSITION, + } + + @Getter + private final Timeline timeline; + @Getter + private final Path timePath; + @Getter + private final Path positionPath; + + @Getter + private EntityPositionTracker entityTracker; + private InterpolatorType defaultInterpolatorType = InterpolatorType.CUBIC; + + public SPTimeline() { + this(createInitialTimeline()); + } + + public SPTimeline(Timeline timeline) { + this.timeline = timeline; + this.timePath = timeline.getPaths().get(SPPath.TIME.ordinal()); + this.positionPath = timeline.getPaths().get(SPPath.POSITION.ordinal()); + } + + public Path getPath(SPPath path) { + switch (path) { + case TIME: + return getTimePath(); + case POSITION: + return getPositionPath(); + } + throw new IllegalArgumentException("Unknown path " + path); + } + + public Keyframe getKeyframe(SPPath path, long keyframe) { + return getPath(path).getKeyframe(keyframe); + } + + public void setEntityTracker(EntityPositionTracker entityTracker) { + Preconditions.checkState(this.entityTracker == null, "Entity tracker already set"); + this.entityTracker = entityTracker; + } + + public void setDefaultInterpolatorType(InterpolatorType defaultInterpolatorType) { + Validate.isTrue(defaultInterpolatorType != InterpolatorType.DEFAULT, "Must not be DEFAULT"); + this.defaultInterpolatorType = Validate.notNull(defaultInterpolatorType); + + if (entityTracker != null) { + timeline.pushChange(updateInterpolators()); + } + } + + public boolean isTimeKeyframe(long time) { + return timePath.getKeyframe(time) != null; + } + + public boolean isPositionKeyframe(long time) { + return positionPath.getKeyframe(time) != null; + } + + public boolean isSpectatorKeyframe(long time) { + Keyframe keyframe = positionPath.getKeyframe(time); + return keyframe != null && keyframe.getValue(SpectatorProperty.PROPERTY).isPresent(); + } + + public void addPositionKeyframe(long time, double posX, double posY, double posZ, + float yaw, float pitch, float roll, int spectated) { + LOGGER.debug("Adding position keyframe at {} pos {}/{}/{} rot {}/{}/{} entId {}", + time, posX, posY, posZ, yaw, pitch, roll, spectated); + + Path path = positionPath; + + Preconditions.checkState(positionPath.getKeyframe(time) == null, "Keyframe already exists"); + + Change change = AddKeyframe.create(path, time); + change.apply(timeline); + Keyframe keyframe = path.getKeyframe(time); + + UpdateKeyframeProperties.Builder builder = UpdateKeyframeProperties.create(path, keyframe); + builder.setValue(CameraProperties.POSITION, Triple.of(posX, posY, posZ)); + builder.setValue(CameraProperties.ROTATION, Triple.of(yaw, pitch, roll)); + if (spectated != -1) { + builder.setValue(SpectatorProperty.PROPERTY, spectated); + } + UpdateKeyframeProperties updateChange = builder.done(); + updateChange.apply(timeline); + change = CombinedChange.createFromApplied(change, updateChange); + + // If this new keyframe formed the first segment of the path + if (path.getSegments().size() == 1) { + // then create an initial interpolator of default type + PathSegment segment = path.getSegments().iterator().next(); + Interpolator interpolator = createDefaultInterpolator(); + SetInterpolator setInterpolator = SetInterpolator.create(segment, interpolator); + setInterpolator.apply(timeline); + change = CombinedChange.createFromApplied(change, setInterpolator); + } + + // Update interpolators for spectator keyframes + // while this is overkill, it is far simpler than updating differently for every possible case + change = CombinedChange.createFromApplied(change, updateInterpolators()); + + Change specPosUpdate = updateSpectatorPositions(); + specPosUpdate.apply(timeline); + change = CombinedChange.createFromApplied(change, specPosUpdate); + + timeline.pushChange(change); + } + + public Change updatePositionKeyframe(long time, double posX, double posY, double posZ, + float yaw, float pitch, float roll) { + LOGGER.debug("Updating position keyframe at {} to pos {}/{}/{} rot {}/{}/{}", + time, posX, posY, posZ, yaw, pitch, roll); + + Keyframe keyframe = positionPath.getKeyframe(time); + + Preconditions.checkState(keyframe != null, "Keyframe does not exists"); + Preconditions.checkState(!keyframe.getValue(SpectatorProperty.PROPERTY).isPresent(), "Cannot update spectator keyframe"); + + Change change = UpdateKeyframeProperties.create(positionPath, keyframe) + .setValue(CameraProperties.POSITION, Triple.of(posX, posY, posZ)) + .setValue(CameraProperties.ROTATION, Triple.of(yaw, pitch, roll)) + .done(); + change.apply(timeline); + return change; + } + + public void removePositionKeyframe(long time) { + LOGGER.debug("Removing position keyframe at {}", time); + + Path path = positionPath; + Keyframe keyframe = path.getKeyframe(time); + + Preconditions.checkState(keyframe != null, "No keyframe at that time"); + + Change change = create(path, keyframe); + change.apply(timeline); + + // Update interpolators for spectator keyframes + // while this is overkill, it is far simpler than updating differently for every possible case + change = CombinedChange.createFromApplied(change, updateInterpolators()); + + Change specPosUpdate = updateSpectatorPositions(); + specPosUpdate.apply(timeline); + change = CombinedChange.createFromApplied(change, specPosUpdate); + + timeline.pushChange(change); + } + + public void addTimeKeyframe(long time, int replayTime) { + LOGGER.debug("Adding time keyframe at {} time {}", time, replayTime); + + Path path = timePath; + + Preconditions.checkState(path.getKeyframe(time) == null, "Keyframe already exists"); + + Change change = AddKeyframe.create(path, time); + change.apply(timeline); + Keyframe keyframe = path.getKeyframe(time); + + UpdateKeyframeProperties updateChange = UpdateKeyframeProperties.create(path, keyframe) + .setValue(TimestampProperty.PROPERTY, replayTime) + .done(); + updateChange.apply(timeline); + change = CombinedChange.createFromApplied(change, updateChange); + + // If this new keyframe formed the first segment of the path + if (path.getSegments().size() == 1) { + // then create an initial interpolator + PathSegment segment = path.getSegments().iterator().next(); + Interpolator interpolator = new LinearInterpolator(); + interpolator.registerProperty(TimestampProperty.PROPERTY); + SetInterpolator setInterpolator = SetInterpolator.create(segment, interpolator); + setInterpolator.apply(timeline); + change = CombinedChange.createFromApplied(change, setInterpolator); + } + + Change specPosUpdate = updateSpectatorPositions(); + specPosUpdate.apply(timeline); + change = CombinedChange.createFromApplied(change, specPosUpdate); + + timeline.pushChange(change); + } + + public Change updateTimeKeyframe(long time, int replayTime) { + LOGGER.debug("Updating time keyframe at {} to time {}", time, replayTime); + + Keyframe keyframe = timePath.getKeyframe(time); + + Preconditions.checkState(keyframe != null, "Keyframe does not exists"); + + Change change = UpdateKeyframeProperties.create(timePath, keyframe) + .setValue(TimestampProperty.PROPERTY, replayTime) + .done(); + change.apply(timeline); + return change; + } + + public void removeTimeKeyframe(long time) { + LOGGER.debug("Removing time keyframe at {}", time); + + Path path = timePath; + Keyframe keyframe = path.getKeyframe(time); + + Preconditions.checkState(keyframe != null, "No keyframe at that time"); + + Change change = create(path, keyframe); + change.apply(timeline); + + Change specPosUpdate = updateSpectatorPositions(); + specPosUpdate.apply(timeline); + change = CombinedChange.createFromApplied(change, specPosUpdate); + + timeline.pushChange(change); + } + + public Change setInterpolatorToDefault(long time) { + LOGGER.debug("Setting interpolator of position keyframe at {} to the default", time); + + Keyframe keyframe = positionPath.getKeyframe(time); + + Preconditions.checkState(keyframe != null, "Keyframe does not exists"); + + Change change = UpdateKeyframeProperties.create(positionPath, keyframe) + .removeProperty(ExplicitInterpolationProperty.PROPERTY) + .done(); + change.apply(timeline); + return CombinedChange.createFromApplied(change, updateInterpolators()); + } + + public Change setInterpolator(long time, Interpolator interpolator) { + LOGGER.debug("Setting interpolator of position keyframe at {} to {}", time, interpolator); + + Keyframe keyframe = positionPath.getKeyframe(time); + Preconditions.checkState(keyframe != null, "Keyframe does not exists"); + PathSegment segment = positionPath.getSegments().stream().filter(s -> s.getStartKeyframe() == keyframe) + .findFirst().orElseThrow(() -> new IllegalStateException("Keyframe has no following segment.")); + + registerPositionInterpolatorProperties(interpolator); + + Change change = CombinedChange.create( + UpdateKeyframeProperties.create(positionPath, keyframe) + .setValue(ExplicitInterpolationProperty.PROPERTY, ObjectUtils.NULL) + .done(), + SetInterpolator.create(segment, interpolator) + ); + change.apply(timeline); + return CombinedChange.createFromApplied(change, updateInterpolators()); + } + + public Change moveKeyframe(SPPath spPath, long oldTime, long newTime) { + LOGGER.debug("Moving keyframe on {} from {} to {}", spPath, oldTime, newTime); + Path path = getPath(spPath); + Keyframe keyframe = path.getKeyframe(oldTime); + + Preconditions.checkState(keyframe != null, "No keyframe at specified time"); + + // Interpolator of the first segment might be required later if it is the only one + Optional firstInterpolator = + path.getSegments().stream().findFirst().map(PathSegment::getInterpolator); + + // The interpolator of the previous segment + Optional interpolatorBefore = + path.getSegments().stream().filter(s -> s.getEndKeyframe() == keyframe).findFirst().map(PathSegment::getInterpolator); + + // The interpolator of the following segment + Optional interpolatorAfter = + path.getSegments().stream().filter(s -> s.getStartKeyframe() == keyframe).findFirst().map(PathSegment::getInterpolator); + + // First remove the old keyframe + Change removeChange = create(path, keyframe); + removeChange.apply(timeline); + + // and add a new one at the correct time + Change addChange = AddKeyframe.create(path, newTime); + addChange.apply(timeline); + + // Then copy over all properties + UpdateKeyframeProperties.Builder builder = UpdateKeyframeProperties.create(path, path.getKeyframe(newTime)); + for (Property property : keyframe.getProperties()) { + copyProperty(property, keyframe, builder); + } + Change propertyChange = builder.done(); + propertyChange.apply(timeline); + + + Change restoreInterpolatorChange; + Keyframe newKf = path.getKeyframe(newTime); + if (Iterables.getLast(path.getKeyframes()) != newKf) { // Unless this is the last keyframe + // the interpolator of the following segment has been lost and needs to be restored + restoreInterpolatorChange = interpolatorAfter.flatMap(interpolator -> + path.getSegments().stream().filter(s -> s.getStartKeyframe() == newKf).findFirst().map(segment -> + SetInterpolator.create(segment, interpolator) + ) + ).orElseGet(CombinedChange::create); + } else { // If it is the last keyframe however, + // the interpolator of the previous segment has been lost and needs to be restored + restoreInterpolatorChange = interpolatorBefore.flatMap(interpolator -> + path.getSegments().stream().filter(s -> s.getEndKeyframe() == newKf).findFirst().map(segment -> + SetInterpolator.create(segment, interpolator) + ) + ).orElseGet(CombinedChange::create); + } + restoreInterpolatorChange.apply(timeline); + + // Finally update the interpolators + Change interpolatorUpdateChange; + if (spPath == SPPath.POSITION) { + // Position / Spectator keyframes need special handling + interpolatorUpdateChange = updateInterpolators(); + } else { + // Time keyframes only need updating when only one segment of them exists + if (path.getSegments().size() == 1) { + assert firstInterpolator.isPresent() : "One segment should have existed before as well"; + interpolatorUpdateChange = SetInterpolator.create(path.getSegments().iterator().next(), firstInterpolator.get()); + } else { + interpolatorUpdateChange = CombinedChange.create(); // Noop change + } + interpolatorUpdateChange.apply(timeline); + } + + // and update spectator positions + Change spectatorChange = updateSpectatorPositions(); + spectatorChange.apply(timeline); + + return CombinedChange.createFromApplied(removeChange, addChange, propertyChange, + restoreInterpolatorChange, interpolatorUpdateChange, spectatorChange); + } + + // Helper method because generics cannot be defined on blocks + private void copyProperty(Property property, Keyframe from, UpdateKeyframeProperties.Builder to) { + from.getValue(property).ifPresent(value -> to.setValue(property, value)); + } + + private Change updateInterpolators() { + Collection pathSegments = positionPath.getSegments(); + // Contains updated pathsegment-interpolator mappings, may be changed multiple times over the course of this method + Map updates = new HashMap<>(); + + // First, setup spectator interpolators + Interpolator interpolator = null; + // Iterate through all segments + for (PathSegment segment : pathSegments) { + if (isSpectatorSegment(segment)) { + // If the last segment was a spectator segment, then use its interpolator for this segment as well + if (interpolator == null) { + // otherwise create a new interpolator + interpolator = new LinearInterpolator(); + interpolator.registerProperty(SpectatorProperty.PROPERTY); + } + // Now that we have an interpolator, set it for the current segment + updates.put(segment, interpolator); + } else { + // Not a spectator segment, make sure we don't re-use the last interpolator (it would not be continuous) + interpolator = null; + } + } + + // Then, if required, replace old default interpolators with new ones + pathSegments.stream() + // Ignore explicitly set segments + .filter(s -> !s.getStartKeyframe().getValue(ExplicitInterpolationProperty.PROPERTY).isPresent()) + // Ignore spectator segments + .filter(s -> !isSpectatorSegment(s)) + // Ignore already correct segments, should ignore all if default hasn't changed + .filter(s -> !s.getInterpolator().getClass().equals(defaultInterpolatorType.getInterpolatorClass())) + // Finally, set the interpolators + // This will create one interpolator per segment, they will be merged later + .forEach(segment -> updates.put(segment, createDefaultInterpolator())); + + // All interpolators should now be set appropriately but may still be fragmented. + // Cleaning that up is a two step process + + // Firstly, all interpolators that are not continuous need to be split up + Interpolator lastInterpolator = null; + Set used = Collections.newSetFromMap(new IdentityHashMap<>()); + for (PathSegment segment : pathSegments) { + if (isSpectatorSegment(segment)) { + lastInterpolator = null; + continue; // Chain broken by spectator segment + } + + Interpolator currentInterpolator = updates.getOrDefault(segment, segment.getInterpolator()); + if (lastInterpolator == currentInterpolator) { + continue; // All fine, interpolator is still continuous + } + + // New interpolator, make sure it hasn't been used before + if (!used.add(interpolator)) { + // It has been used before, we need to clone it and use the clone instead + // This will create a new interpolator for each segment which will be merged later + currentInterpolator = cloneInterpolator(currentInterpolator); + updates.put(segment, currentInterpolator); + } + lastInterpolator = currentInterpolator; + } + + // Secondly, all neighbouring interpolators that are equal need to be merged + lastInterpolator = null; + String lastInterpolatorSerialized = null; + for (PathSegment segment : pathSegments) { + if (isSpectatorSegment(segment)) { + lastInterpolator = null; + lastInterpolatorSerialized = null; + continue; // Spectator segments are continuous by construction + } + Interpolator currentInterpolator = updates.getOrDefault(segment, segment.getInterpolator()); + String serialized = serializeInterpolator(currentInterpolator); + if (lastInterpolator != currentInterpolator && serialized.equals(lastInterpolatorSerialized)) { + // This interpolator is equal to the last one but not the same, needs merging + updates.put(segment, lastInterpolator); + continue; + } + // New interpolator, not related to the previous one, merged by definition + lastInterpolator = currentInterpolator; + lastInterpolatorSerialized = serialized; + } + + Change change = CombinedChange.create(updates.entrySet().stream() + .map(e -> SetInterpolator.create(e.getKey(), e.getValue())).toArray(Change[]::new)); + change.apply(timeline); + return change; + } + + private boolean isSpectatorSegment(PathSegment segment) { + return segment.getStartKeyframe().getValue(SpectatorProperty.PROPERTY).isPresent() + && segment.getEndKeyframe().getValue(SpectatorProperty.PROPERTY).isPresent(); + } + + private Change updateSpectatorPositions() { + List changes = new ArrayList<>(); + timePath.updateAll(); + for (Keyframe keyframe : positionPath.getKeyframes()) { + Optional spectator = keyframe.getValue(SpectatorProperty.PROPERTY); + if (spectator.isPresent()) { + Optional time = timePath.getValue(TimestampProperty.PROPERTY, keyframe.getTime()); + if (!time.isPresent()) { + continue; // No time keyframes set at this video time, cannot determine replay time + } + Location expected = entityTracker.getEntityPositionAtTimestamp(spectator.get(), time.get()); + if (expected == null) { + continue; // We don't have any data on this entity for some reason + } + Triple pos = keyframe.getValue(CameraProperties.POSITION).orElse(Triple.of(0D, 0D, 0D)); + Triple rot = keyframe.getValue(CameraProperties.ROTATION).orElse(Triple.of(0F, 0F, 0F)); + Location actual = new Location(pos.getLeft(), pos.getMiddle(), pos.getRight(), rot.getLeft(), rot.getRight()); + if (!expected.equals(actual)) { + changes.add(UpdateKeyframeProperties.create(positionPath, keyframe) + .setValue(CameraProperties.POSITION, Triple.of(expected.getX(), expected.getY(), expected.getZ())) + .setValue(CameraProperties.ROTATION, Triple.of(expected.getYaw(), expected.getPitch(), 0f)).done() + ); + } + } + } + return CombinedChange.create(changes.toArray(new Change[changes.size()])); + } + + private Interpolator createDefaultInterpolator() { + return registerPositionInterpolatorProperties(defaultInterpolatorType.newInstance()); + } + + private Interpolator registerPositionInterpolatorProperties(Interpolator interpolator) { + interpolator.registerProperty(CameraProperties.POSITION); + interpolator.registerProperty(CameraProperties.ROTATION); + return interpolator; + } + + @Override + public Timeline createTimeline() { + return createTimelineStatic(); + } + + private static Timeline createInitialTimeline() { + Timeline timeline = createTimelineStatic(); + timeline.createPath(); + timeline.createPath(); + return timeline; + } + + private static Timeline createTimelineStatic() { + Timeline timeline = new TimelineImpl(); + + timeline.registerProperty(TimestampProperty.PROPERTY); + timeline.registerProperty(CameraProperties.POSITION); + timeline.registerProperty(CameraProperties.ROTATION); + timeline.registerProperty(SpectatorProperty.PROPERTY); + timeline.registerProperty(ExplicitInterpolationProperty.PROPERTY); + + return timeline; + } + + @Override + public void serializeInterpolator(JsonWriter writer, Interpolator interpolator) throws IOException { + if (interpolator instanceof LinearInterpolator) { + writer.value("linear"); + } else if (interpolator instanceof CubicSplineInterpolator) { + writer.value("cubic-spline"); + } else { + throw new IOException("Unknown interpolator type: " + interpolator); + } + } + + @Override + public Interpolator deserializeInterpolator(JsonReader reader) throws IOException { + String type = reader.nextString(); + switch (type) { + case "linear": + return new LinearInterpolator(); + case "cubic-spline": + return new CubicSplineInterpolator(); + default: + throw new IOException("Unknown interpolation type: " + type); + + } + } + + /** + * Clones an interpolator by de- and re-serializing it. + * @param interpolator The interpolator to clone + * @return The cloned interpolator + */ + private Interpolator cloneInterpolator(Interpolator interpolator) { + Interpolator cloned = deserializeInterpolator(serializeInterpolator(interpolator)); + interpolator.getKeyframeProperties().forEach(cloned::registerProperty); + return cloned; + } + + /** + * Serializes the specific interpolator to String. + * Does not serialize the registered keyframe properties. + * @param interpolator The interpolator to serialize. + * @return The serialized interpolator + */ + private String serializeInterpolator(Interpolator interpolator) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + JsonWriter jsonWriter = new JsonWriter(new PrintWriter(baos)); + try { + jsonWriter.beginArray(); + serializeInterpolator(jsonWriter, interpolator); + jsonWriter.endArray(); + jsonWriter.flush(); + } catch (IOException e) { + CrashReport crash = CrashReport.makeCrashReport(e, "Serializing interpolator"); + CrashReportCategory category = crash.makeCategory("Serializing interpolator"); + category.addCrashSectionCallable("Interpolator", interpolator::toString); + throw new ReportedException(crash); + } + + return baos.toString(); + } + + private Interpolator deserializeInterpolator(String json) { + JsonReader jsonReader = new JsonReader(new StringReader(json)); + try { + jsonReader.beginArray(); + return deserializeInterpolator(jsonReader); + } catch (IOException e) { + CrashReport crash = CrashReport.makeCrashReport(e, "De-serializing interpolator"); + CrashReportCategory category = crash.makeCategory("De-serializing interpolator"); + category.addCrashSection("Interpolator", json); + throw new ReportedException(crash); + } + } +} diff --git a/src/main/java/com/replaymod/simplepathing/gui/GuiEditKeyframe.java b/src/main/java/com/replaymod/simplepathing/gui/GuiEditKeyframe.java index d1c57a8f..9cabba88 100644 --- a/src/main/java/com/replaymod/simplepathing/gui/GuiEditKeyframe.java +++ b/src/main/java/com/replaymod/simplepathing/gui/GuiEditKeyframe.java @@ -1,23 +1,29 @@ package com.replaymod.simplepathing.gui; import com.replaymod.pathing.properties.CameraProperties; -import com.replaymod.pathing.properties.ExplicitInterpolationProperty; +import com.replaymod.simplepathing.properties.ExplicitInterpolationProperty; import com.replaymod.pathing.properties.TimestampProperty; import com.replaymod.replay.ReplayModReplay; import com.replaymod.replaystudio.pathing.change.Change; import com.replaymod.replaystudio.pathing.change.CombinedChange; -import com.replaymod.replaystudio.pathing.change.SetInterpolator; -import com.replaymod.replaystudio.pathing.change.UpdateKeyframeProperties; import com.replaymod.replaystudio.pathing.interpolation.CubicSplineInterpolator; import com.replaymod.replaystudio.pathing.interpolation.Interpolator; import com.replaymod.replaystudio.pathing.interpolation.LinearInterpolator; import com.replaymod.replaystudio.pathing.path.Keyframe; import com.replaymod.replaystudio.pathing.path.Path; import com.replaymod.replaystudio.pathing.path.PathSegment; +import com.replaymod.simplepathing.InterpolatorType; +import com.replaymod.simplepathing.SPTimeline; +import com.replaymod.simplepathing.SPTimeline.SPPath; import com.replaymod.simplepathing.Setting; -import com.replaymod.simplepathing.gui.GuiEditKeyframe.Position.InterpolationPanel.InterpolatorType; +import de.johni0702.minecraft.gui.container.AbstractGuiContainer; import de.johni0702.minecraft.gui.container.GuiPanel; -import de.johni0702.minecraft.gui.element.*; +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.GuiTooltip; +import de.johni0702.minecraft.gui.element.IGuiClickable; +import de.johni0702.minecraft.gui.element.IGuiLabel; import de.johni0702.minecraft.gui.element.advanced.GuiDropdownMenu; import de.johni0702.minecraft.gui.function.Typeable; import de.johni0702.minecraft.gui.layout.GridLayout; @@ -26,19 +32,16 @@ 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 lombok.AllArgsConstructor; import lombok.Getter; import net.minecraft.client.resources.I18n; -import org.apache.commons.lang3.tuple.Triple; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.lwjgl.input.Keyboard; import org.lwjgl.util.ReadablePoint; import java.util.Map; +import java.util.Optional; -import static com.replaymod.simplepathing.gui.GuiEditKeyframe.Position.InterpolationPanel.InterpolatorSettingsPanel.CubicInterpolatorSettingsPanel; -import static com.replaymod.simplepathing.gui.GuiEditKeyframe.Position.InterpolationPanel.InterpolatorSettingsPanel.LinearInterpolatorSettingsPanel; import static de.johni0702.minecraft.gui.utils.Utils.link; public abstract class GuiEditKeyframe> extends AbstractGuiPopup implements Typeable { @@ -50,6 +53,7 @@ public abstract class GuiEditKeyframe> extends Abst protected final GuiPathing guiPathing; + protected final long time; protected final Keyframe keyframe; protected final Path path; @@ -85,13 +89,13 @@ public abstract class GuiEditKeyframe> extends Abst .addElements(new VerticalLayout.Data(0.5, false), title, inputs, timePanel, buttons); } - public GuiEditKeyframe(GuiPathing gui, Path path, Keyframe keyframe, String type) { + public GuiEditKeyframe(GuiPathing gui, SPPath path, long time, String type) { super(ReplayModReplay.instance.getReplayHandler().getOverlay()); this.guiPathing = gui; - this.keyframe = keyframe; - this.path = path; + this.time = time; + this.path = gui.getMod().getCurrentTimeline().getPath(path); + this.keyframe = this.path.getKeyframe(time); - long time = keyframe.getTime(); Consumer updateSaveButtonState = s -> saveButton.setEnabled(canSave()); timeMinField.setValue(time / 1000 / 60).onTextChanged(updateSaveButtonState); timeSecField.setValue(time / 1000 % 60).onTextChanged(updateSaveButtonState); @@ -101,10 +105,11 @@ public abstract class GuiEditKeyframe> extends Abst saveButton.onClick(() -> { Change change = save(); long newTime = (timeMinField.getInteger() * 60 + timeSecField.getInteger()) * 1000 + timeMSecField.getInteger(); - if (newTime != keyframe.getTime()) { - change = CombinedChange.createFromApplied(change, gui.moveKeyframe(path, keyframe, newTime)); + if (newTime != time) { + change = CombinedChange.createFromApplied(change, + gui.getMod().getCurrentTimeline().moveKeyframe(path, time, newTime)); } - path.getTimeline().pushChange(change); + gui.getMod().getCurrentTimeline().getTimeline().pushChange(change); close(); }); } @@ -134,7 +139,7 @@ public abstract class GuiEditKeyframe> extends Abst protected abstract Change save(); public static class Spectator extends GuiEditKeyframe { - public Spectator(GuiPathing gui, Path path, Keyframe keyframe) { + public Spectator(GuiPathing gui, SPPath path, long keyframe) { super(gui, path, keyframe, "spec"); link(timeMinField, timeSecField, timeMSecField); @@ -167,10 +172,10 @@ public abstract class GuiEditKeyframe> extends Abst timestampMSecField, new GuiLabel().setI18nText("replaymod.gui.milliseconds")); } - public Time(GuiPathing gui, Path path, Keyframe keyframe) { + public Time(GuiPathing gui, SPPath path, long keyframe) { super(gui, path, keyframe, "time"); - keyframe.getValue(TimestampProperty.PROPERTY).ifPresent(time -> { + this.keyframe.getValue(TimestampProperty.PROPERTY).ifPresent(time -> { timestampMinField.setValue(time / 1000 / 60); timestampSecField.setValue(time / 1000 % 60); timestampMSecField.setValue(time % 1000); @@ -185,11 +190,7 @@ public abstract class GuiEditKeyframe> extends Abst protected Change save() { int time = (timestampMinField.getInteger() * 60 + timestampSecField.getInteger()) * 1000 + timestampMSecField.getInteger(); - Change change = UpdateKeyframeProperties.create(path, keyframe) - .setValue(TimestampProperty.PROPERTY, time) - .done(); - change.apply(path.getTimeline()); - return change; + return guiPathing.getMod().getCurrentTimeline().updateTimeKeyframe(keyframe.getTime(), time); } @Override @@ -207,7 +208,7 @@ public abstract class GuiEditKeyframe> extends Abst public final GuiNumberField pitchField = newGuiNumberField().setSize(60, 20).setPrecision(5); public final GuiNumberField rollField = newGuiNumberField().setSize(60, 20).setPrecision(5); - public final InterpolationPanel interpolationPanel = new InterpolationPanel(guiPathing); + public final InterpolationPanel interpolationPanel = new InterpolationPanel(); { GuiPanel positionInputs = new GuiPanel() @@ -224,15 +225,15 @@ public abstract class GuiEditKeyframe> extends Abst positionInputs, interpolationPanel); } - public Position(GuiPathing gui, Path path, Keyframe keyframe) { + public Position(GuiPathing gui, SPPath path, long keyframe) { super(gui, path, keyframe, "pos"); - keyframe.getValue(CameraProperties.POSITION).ifPresent(pos -> { + this.keyframe.getValue(CameraProperties.POSITION).ifPresent(pos -> { xField.setValue(pos.getLeft()); yField.setValue(pos.getMiddle()); zField.setValue(pos.getRight()); }); - keyframe.getValue(CameraProperties.ROTATION).ifPresent(rot -> { + this.keyframe.getValue(CameraProperties.ROTATION).ifPresent(rot -> { yawField.setValue(rot.getLeft()); pitchField.setValue(rot.getMiddle()); rollField.setValue(rot.getRight()); @@ -245,48 +246,17 @@ public abstract class GuiEditKeyframe> extends Abst @Override protected Change save() { - Change setInterpolatorChange = null; - - UpdateKeyframeProperties.Builder builder = UpdateKeyframeProperties.create(path, keyframe) - .setValue(CameraProperties.POSITION, Triple.of(xField.getDouble(), yField.getDouble(), zField.getDouble())) - .setValue(CameraProperties.ROTATION, Triple.of(yawField.getFloat(), pitchField.getFloat(), rollField.getFloat())) - .removeProperty(ExplicitInterpolationProperty.PROPERTY); - - // if the interpolator is not the default, set the ExplicitInterpolationProperty flag - if (interpolationPanel.getInterpolatorType() != InterpolatorType.DEFAULT) { - PathSegment toModify = null; - for (PathSegment segment : path.getSegments()) { - if (segment.getStartKeyframe() == keyframe) { - toModify = segment; - break; - } - } - - if (toModify != null) { - builder.setValue(ExplicitInterpolationProperty.PROPERTY, new Object()); - - Interpolator interpolator = interpolationPanel.getInterpolatorSettingsPanel().createInterpolator(); - interpolator.registerProperty(CameraProperties.POSITION); - interpolator.registerProperty(CameraProperties.ROTATION); - - setInterpolatorChange = SetInterpolator.create(toModify, interpolator); - } else { - logger.warn("The Path segment to modify was not found. Setting interpolator to default."); - } - } - - Change keyframePropertiesChange = builder.done(); - keyframePropertiesChange.apply(path.getTimeline()); - - if (setInterpolatorChange == null) { - return keyframePropertiesChange; + SPTimeline timeline = guiPathing.getMod().getCurrentTimeline(); + Change positionChange = timeline.updatePositionKeyframe(time, + xField.getDouble(), yField.getDouble(), zField.getDouble(), + yawField.getFloat(), pitchField.getFloat(), rollField.getFloat() + ); + if (interpolationPanel.getInterpolatorType() == InterpolatorType.DEFAULT) { + return CombinedChange.createFromApplied(positionChange, timeline.setInterpolatorToDefault(time)); } else { - setInterpolatorChange.apply(path.getTimeline()); - guiPathing.updateInterpolators(); - path.updateAll(); + Interpolator interpolator = interpolationPanel.getSettingsPanel().createInterpolator(); + return CombinedChange.createFromApplied(positionChange, timeline.setInterpolator(time, interpolator)); } - - return CombinedChange.createFromApplied(keyframePropertiesChange, setInterpolatorChange); } @Override @@ -294,51 +264,20 @@ public abstract class GuiEditKeyframe> extends Abst return this; } - public static class InterpolationPanel extends de.johni0702.minecraft.gui.container.AbstractGuiContainer { - - private final GuiPathing guiPathing; + public class InterpolationPanel extends AbstractGuiContainer { @Getter - private InterpolatorSettingsPanel interpolatorSettingsPanel; + private SettingsPanel settingsPanel; private GuiDropdownMenu dropdown; - @AllArgsConstructor - public enum InterpolatorType { - DEFAULT("default", null), - CUBIC("cubic", CubicSplineInterpolator.class), - LINEAR("linear", LinearInterpolator.class); - - private String localizationKey; - - @Getter - private Class interpolatorClass; - - @Override - public String toString() { - return I18n.format(String.format("replaymod.gui.editkeyframe.interpolator.%1$s.name", localizationKey)); - } - - public String getI18nDescription() { - return String.format("replaymod.gui.editkeyframe.interpolator.%1$s.desc", localizationKey); - } - - public static InterpolatorType fromString(String string) { - for (InterpolatorType t : values()) { - if (t.toString().equals(string)) return t; - } - return CUBIC; //the default - } - - } - - public InterpolationPanel(GuiPathing guiPathing) { - this.guiPathing = guiPathing; - + public InterpolationPanel() { setLayout(new VerticalLayout()); - dropdown = new GuiDropdownMenu().setValues(InterpolatorType.values()).setHeight(20); - dropdown.onSelection((index) -> setSettingsPanel(dropdown.getSelectedValue())); + dropdown = new GuiDropdownMenu() + .setToString(s -> I18n.format(s.getI18nName())) + .setValues(InterpolatorType.values()).setHeight(20) + .onSelection(i -> setSettingsPanel(dropdown.getSelectedValue())); // set hover tooltips for (Map.Entry e : dropdown.getDropdownEntries().entrySet()) { @@ -353,25 +292,35 @@ public abstract class GuiEditKeyframe> extends Abst addElements(new VerticalLayout.Data(0.5, false), dropdownPanel); - dropdown.onSelection(0); // trigger the callback once to display settings panel + Optional segment = path.getSegments().stream() + .filter(s -> s.getStartKeyframe() == keyframe).findFirst(); + if (segment.isPresent()) { + if (keyframe.getValue(ExplicitInterpolationProperty.PROPERTY).isPresent()) { + Interpolator interpolator = segment.get().getInterpolator(); + InterpolatorType type = InterpolatorType.fromClass(interpolator.getClass()); + dropdown.setSelected(type); // trigger the callback once to display settings panel + } else { + setSettingsPanel(InterpolatorType.DEFAULT); + } + } else { + // Disable dropdown if this is the last keyframe + dropdown.setDisabled(); + } } public void setSettingsPanel(InterpolatorType type) { - removeElement(this.interpolatorSettingsPanel); + removeElement(this.settingsPanel); - InterpolatorSettingsPanel settingsPanel = null; switch (getInterpolatorTypeNoDefault(type)) { case CUBIC: - settingsPanel = new CubicInterpolatorSettingsPanel(); + settingsPanel = new CubicSettingsPanel(); break; case LINEAR: - settingsPanel = new LinearInterpolatorSettingsPanel(); + settingsPanel = new LinearSettingsPanel(); break; } addElements(new GridLayout.Data(0.5, 0.5), settingsPanel); - - this.interpolatorSettingsPanel = settingsPanel; } protected InterpolatorType getInterpolatorTypeNoDefault(InterpolatorType interpolatorType) { @@ -392,44 +341,44 @@ public abstract class GuiEditKeyframe> extends Abst return this; } - public static abstract class InterpolatorSettingsPanel extends de.johni0702.minecraft.gui.container.GuiPanel { + public abstract class SettingsPanel> extends AbstractGuiContainer { public abstract void loadSettings(I interpolator); public abstract I createInterpolator(); + } - public static class CubicInterpolatorSettingsPanel extends InterpolatorSettingsPanel { + public class CubicSettingsPanel extends SettingsPanel { - @Override - public void loadSettings(CubicSplineInterpolator interpolator) { - } - - @Override - public CubicSplineInterpolator createInterpolator() { - return new CubicSplineInterpolator(); - } - - @Override - protected InterpolatorSettingsPanel getThis() { - return this; - } + @Override + public void loadSettings(CubicSplineInterpolator interpolator) { } - public static class LinearInterpolatorSettingsPanel extends InterpolatorSettingsPanel { + @Override + public CubicSplineInterpolator createInterpolator() { + return new CubicSplineInterpolator(); + } - @Override - public void loadSettings(LinearInterpolator interpolator) { - } + @Override + protected CubicSettingsPanel getThis() { + return this; + } + } - @Override - public LinearInterpolator createInterpolator() { - return new LinearInterpolator(); - } + public class LinearSettingsPanel extends SettingsPanel { - @Override - protected InterpolatorSettingsPanel getThis() { - return this; - } + @Override + public void loadSettings(LinearInterpolator interpolator) { + } + + @Override + public LinearInterpolator createInterpolator() { + return new LinearInterpolator(); + } + + @Override + protected LinearSettingsPanel getThis() { + return this; } } } diff --git a/src/main/java/com/replaymod/simplepathing/gui/GuiKeyframeTimeline.java b/src/main/java/com/replaymod/simplepathing/gui/GuiKeyframeTimeline.java index 72f1368c..d0285924 100644 --- a/src/main/java/com/replaymod/simplepathing/gui/GuiKeyframeTimeline.java +++ b/src/main/java/com/replaymod/simplepathing/gui/GuiKeyframeTimeline.java @@ -1,7 +1,5 @@ package com.replaymod.simplepathing.gui; -import com.google.common.base.Function; -import com.google.common.collect.Iterables; import com.replaymod.core.ReplayMod; import com.replaymod.pathing.properties.CameraProperties; import com.replaymod.pathing.properties.SpectatorProperty; @@ -11,9 +9,10 @@ import com.replaymod.replaystudio.pathing.change.Change; import com.replaymod.replaystudio.pathing.path.Keyframe; import com.replaymod.replaystudio.pathing.path.Path; import com.replaymod.replaystudio.pathing.path.PathSegment; -import com.replaymod.replaystudio.pathing.path.Timeline; import com.replaymod.replaystudio.pathing.property.Property; import com.replaymod.simplepathing.ReplayModSimplePathing; +import com.replaymod.simplepathing.SPTimeline; +import com.replaymod.simplepathing.SPTimeline.SPPath; import de.johni0702.minecraft.gui.GuiRenderer; import de.johni0702.minecraft.gui.element.advanced.AbstractGuiTimeline; import de.johni0702.minecraft.gui.function.Draggable; @@ -23,7 +22,6 @@ import org.lwjgl.util.Point; import org.lwjgl.util.ReadableDimension; import org.lwjgl.util.ReadablePoint; -import javax.annotation.Nullable; import java.util.Comparator; import java.util.Optional; @@ -37,14 +35,14 @@ public class GuiKeyframeTimeline extends AbstractGuiTimeline>() { - @Nullable - @Override - public Iterable apply(@Nullable Path input) { - assert input != null; - return input.getKeyframes(); - } - }))) { + SPTimeline timeline = mod.getCurrentTimeline(); + + timeline.getTimeline().getPaths().stream().flatMap(path -> path.getKeyframes().stream()).forEach(keyframe -> { if (keyframe.getTime() >= startTime && keyframe.getTime() <= endTime) { double relativeTime = keyframe.getTime() - startTime; int positonX = BORDER_LEFT + (int) (relativeTime / visibleTime * visibleWidth) - KEYFRAME_SIZE / 2; - int u = KEYFRAME_TEXTURE_X + (mod.getSelectedKeyframe() == keyframe ? KEYFRAME_SIZE : 0); + int u = KEYFRAME_TEXTURE_X + (mod.isSelected(keyframe) ? KEYFRAME_SIZE : 0); int v = KEYFRAME_TEXTURE_Y; if (keyframe.getValue(CameraProperties.POSITION).isPresent()) { if (keyframe.getValue(SpectatorProperty.PROPERTY).isPresent()) { @@ -113,10 +106,10 @@ public class GuiKeyframeTimeline extends AbstractGuiTimeline= startTimestamp) { @@ -162,29 +155,28 @@ public class GuiKeyframeTimeline extends AbstractGuiTimeline getKeyframe(ReadablePoint position) { + private Pair getKeyframe(ReadablePoint position) { int time = getTimeAt(position.getX(), position.getY()); if (time != -1) { Point mouse = new Point(position); getContainer().convertFor(this, mouse); int mouseY = mouse.getY(); if (mouseY > BORDER_TOP && mouseY < BORDER_TOP + 2 * KEYFRAME_SIZE) { - Timeline timeline = gui.getMod().getCurrentTimeline(); - int path; + SPPath path; if (mouseY <= BORDER_TOP + KEYFRAME_SIZE) { // Position keyframe - path = GuiPathing.POSITION_PATH; + path = SPPath.POSITION; } else { // Time keyframe - path = GuiPathing.TIME_PATH; + path = SPPath.TIME; } int visibleTime = (int) (getZoom() * getLength()); - int tolerance = visibleTime * KEYFRAME_SIZE / (size.getWidth() - BORDER_LEFT - BORDER_RIGHT) / 2; - Optional keyframe = timeline.getPaths().get(path).getKeyframes().stream() + int tolerance = visibleTime * KEYFRAME_SIZE / (getLastSize().getWidth() - BORDER_LEFT - BORDER_RIGHT) / 2; + Optional keyframe = gui.getMod().getCurrentTimeline().getPath(path).getKeyframes().stream() .filter(k -> Math.abs(k.getTime() - time) <= tolerance) .sorted(Comparator.comparing(k -> Math.abs(k.getTime() - time))) .findFirst(); - return Pair.of(path, keyframe.orElse(null)); + return Pair.of(path, keyframe.map(Keyframe::getTime).orElse(null)); } } return Pair.of(null, null); @@ -193,30 +185,31 @@ public class GuiKeyframeTimeline extends AbstractGuiTimeline pathKeyframePair = getKeyframe(position); + Pair pathKeyframePair = getKeyframe(position); if (pathKeyframePair.getRight() != null) { + SPPath path = pathKeyframePair.getLeft(); // Clicked on keyframe - Keyframe keyframe = pathKeyframePair.getRight(); + long keyframeTime = pathKeyframePair.getRight(); if (button == 0) { // Left click long now = Minecraft.getSystemTime(); - if (lastClickedKeyframe == keyframe) { + if (lastClickedKeyframe == keyframeTime) { // Clicked the same keyframe again, potentially a double click if (now - lastClickedTime < DOUBLE_CLICK_INTERVAL) { // Yup, double click, open the edit keyframe gui - Path path = gui.getMod().getCurrentTimeline().getPaths().get(pathKeyframePair.getLeft()); - gui.openEditKeyframePopup(path, keyframe); + gui.openEditKeyframePopup(path, keyframeTime); return true; } } // Not a double click, just update the click time and selection lastClickedTime = now; - lastClickedKeyframe = keyframe; - lastClickedPath = pathKeyframePair.getLeft(); - gui.getMod().setSelectedKeyframe(lastClickedKeyframe); + lastClickedKeyframe = keyframeTime; + lastClickedPath = path; + gui.getMod().setSelected(lastClickedPath, lastClickedKeyframe); // We might be dragging draggingStartX = position.getX(); dragging = true; } else if (button == 1) { // Right click + Keyframe keyframe = gui.getMod().getCurrentTimeline().getKeyframe(path, keyframeTime); for (Property property : keyframe.getProperties()) { applyPropertyToGame(property, keyframe); } @@ -226,12 +219,11 @@ public class GuiKeyframeTimeline extends AbstractGuiTimeline k.getProperties().stream()).distinct().forEach( p -> applyPropertyToGame(p, path, time)); } @@ -280,12 +272,13 @@ public class GuiKeyframeTimeline extends AbstractGuiTimeline mouseDrag(position, button, timeSinceLastCall))) return true; // Threshold passed - Path path = gui.getMod().getCurrentTimeline().getPaths().get(lastClickedPath); + SPTimeline timeline = gui.getMod().getCurrentTimeline(); Point mouse = new Point(position); getContainer().convertFor(this, mouse); int mouseX = mouse.getX(); - int width = size.getWidth(); + int width = getLastSize().getWidth(); int bodyWidth = width - BORDER_LEFT - BORDER_RIGHT; double segmentLength = getLength() * getZoom(); double segmentTime = segmentLength * (mouseX - BORDER_LEFT) / bodyWidth; @@ -295,24 +288,21 @@ public class GuiKeyframeTimeline extends AbstractGuiTimeline {}); @@ -296,15 +290,14 @@ public class GuiPathing { if (player.isActive()) { player.getFuture().cancel(false); } else { - Timeline timeline = mod.getCurrentTimeline(); - Path timePath = timeline.getPaths().get(TIME_PATH); + Path timePath = mod.getCurrentTimeline().getTimePath(); if (!preparePathsForPlayback()) return; timePath.setActive(!Keyboard.isKeyDown(Keyboard.KEY_LSHIFT)); // 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); + ListenableFuture future = player.start(mod.getCurrentTimeline().getTimeline(), startTime); overlay.setCloseable(false); overlay.setMouseVisible(true); Futures.addCallback(future, new FutureCallback() { @@ -326,26 +319,28 @@ public class GuiPathing { positionKeyframeButton.setTexturePosH(new ReadablePoint() { @Override public int getX() { - Keyframe keyframe = mod.getSelectedKeyframe(); - if (keyframe == null || !keyframe.getValue(CameraProperties.POSITION).isPresent()) { + SPPath keyframePath = mod.getSelectedPath(); + long keyframeTime = mod.getSelectedTime(); + if (keyframePath != SPPath.POSITION) { // No keyframe or wrong path - keyframe = mod.getCurrentTimeline().getPaths().get(POSITION_PATH).getKeyframe(timeline.getCursorPosition()); + keyframeTime = timeline.getCursorPosition(); + keyframePath = mod.getCurrentTimeline().isPositionKeyframe(keyframeTime) ? SPPath.POSITION : null; } - if (keyframe == null) { + if (keyframePath != SPPath.POSITION) { return replayHandler.isCameraView() ? 0 : 40; } else { - return keyframe.getValue(SpectatorProperty.PROPERTY).isPresent() ? 40 : 0; + return mod.getCurrentTimeline().isSpectatorKeyframe(keyframeTime) ? 40 : 0; } } @Override public int getY() { - Keyframe keyframe = mod.getSelectedKeyframe(); - if (keyframe == null || !keyframe.getValue(CameraProperties.POSITION).isPresent()) { + SPPath keyframePath = mod.getSelectedPath(); + if (keyframePath != SPPath.POSITION) { // No keyframe selected but there might be one at exactly the position of the cursor - keyframe = mod.getCurrentTimeline().getPaths().get(POSITION_PATH).getKeyframe(timeline.getCursorPosition()); + keyframePath = mod.getCurrentTimeline().isPositionKeyframe(timeline.getCursorPosition()) ? SPPath.POSITION : null; } - return keyframe != null && keyframe.getValue(CameraProperties.POSITION).isPresent() ? 60 : 40; + return keyframePath == SPPath.POSITION ? 60 : 40; } @Override @@ -355,7 +350,7 @@ public class GuiPathing { }).onClick(new Runnable() { @Override public void run() { - updateKeyframe(false); + updateKeyframe(SPPath.POSITION); } }); @@ -367,12 +362,12 @@ public class GuiPathing { @Override public int getY() { - Keyframe keyframe = mod.getSelectedKeyframe(); - if (keyframe == null || !keyframe.getValue(TimestampProperty.PROPERTY).isPresent()) { + SPPath keyframePath = mod.getSelectedPath(); + if (keyframePath != SPPath.TIME) { // No keyframe selected but there might be one at exactly the position of the cursor - keyframe = mod.getCurrentTimeline().getPaths().get(TIME_PATH).getKeyframe(timeline.getCursorPosition()); + keyframePath = mod.getCurrentTimeline().isTimeKeyframe(timeline.getCursorPosition()) ? SPPath.TIME : null; } - return keyframe != null && keyframe.getValue(TimestampProperty.PROPERTY).isPresent() ? 100 : 80; + return keyframePath == SPPath.TIME ? 100 : 80; } @Override @@ -382,7 +377,7 @@ public class GuiPathing { }).onClick(new Runnable() { @Override public void run() { - updateKeyframe(true); + updateKeyframe(SPPath.TIME); } }); @@ -404,12 +399,12 @@ public class GuiPathing { } try { GuiKeyframeRepository gui = new GuiKeyframeRepository( - mod, replayHandler.getReplayFile(), mod.getCurrentTimeline()); + mod.getCurrentTimeline(), replayHandler.getReplayFile(), mod.getCurrentTimeline().getTimeline()); Futures.addCallback(gui.getFuture(), new FutureCallback() { @Override public void onSuccess(Timeline result) { if (result != null) { - mod.setCurrentTimeline(result); + mod.setCurrentTimeline(new SPTimeline(result)); } } @@ -435,10 +430,10 @@ public class GuiPathing { @Override public void onSuccess(Boolean delete) { if (delete) { - Timeline timeline = mod.createTimeline(); - timeline.createPath(); - timeline.createPath(); - mod.setCurrentTimeline(timeline); + mod.clearCurrentTimeline(); + if (entityTracker != null) { + mod.getCurrentTimeline().setEntityTracker(entityTracker); + } } } @@ -455,7 +450,7 @@ public class GuiPathing { // Position of the cursor int cursor = timeline.getCursorPosition(); // Get the last time keyframe before the cursor - mod.getCurrentTimeline().getPaths().get(TIME_PATH).getKeyframes().stream() + mod.getCurrentTimeline().getTimePath().getKeyframes().stream() .filter(it -> it.getTime() <= cursor).reduce((__, last) -> last).ifPresent(keyframe -> { // Cursor position at the keyframe int keyframeCursor = (int) keyframe.getTime(); @@ -471,7 +466,7 @@ public class GuiPathing { // Move cursor to new position timeline.setCursorPosition(keyframeCursor + cursorPassed); // Deselect keyframe to allow the user to add a new one right away - mod.setSelectedKeyframe(null); + mod.setSelected(null, 0); }); }); @@ -479,8 +474,8 @@ public class GuiPathing { if (!overlay.isVisible()) { return; } - if (mod.getSelectedKeyframe() != null) { - updateKeyframe(mod.getSelectedKeyframe().getValue(TimestampProperty.PROPERTY).isPresent()); + if (mod.getSelectedPath() != null) { + updateKeyframe(mod.getSelectedPath()); } }); @@ -511,13 +506,12 @@ public class GuiPathing { } private boolean preparePathsForPlayback() { - Timeline timeline = mod.getCurrentTimeline(); - timeline.getPaths().get(TIME_PATH).updateAll(); - timeline.getPaths().get(POSITION_PATH).updateAll(); + SPTimeline timeline = mod.getCurrentTimeline(); + timeline.getTimeline().getPaths().forEach(Path::updateAll); // Make sure time keyframes's values are monotonically increasing int lastTime = 0; - for (Keyframe keyframe : timeline.getPaths().get(TIME_PATH).getKeyframes()) { + for (Keyframe keyframe : timeline.getTimePath().getKeyframes()) { int time = keyframe.getValue(TimestampProperty.PROPERTY).orElseThrow(IllegalStateException::new); if (time < lastTime) { // We are going backwards in time @@ -531,8 +525,8 @@ public class GuiPathing { } // Make sure there are at least two position- and two time-keyframes - if (timeline.getPaths().get(POSITION_PATH).getSegments().isEmpty() - || timeline.getPaths().get(TIME_PATH).getSegments().isEmpty()) { + if (timeline.getPositionPath().getSegments().isEmpty() + || timeline.getTimePath().getSegments().isEmpty()) { GuiInfoPopup.open(replayHandler.getOverlay(), "replaymod.chat.morekeyframes"); return false; } @@ -544,19 +538,19 @@ public class GuiPathing { scrollbar.setZoom(scrollbar.getZoom() * factor); } - /** - * Called when either one of the property buttons is pressed. - * @param isTime {@code true} for the time property button, {@code false} for the place property button - */ - private void updateKeyframe(final boolean isTime) { + public boolean ensureEntityTracker(Runnable withDelayedTracker) { if (entityTracker == null) { + LOGGER.debug("Entity tracker not yet loaded, delaying..."); LoadEntityTrackerPopup popup = new LoadEntityTrackerPopup(replayHandler.getOverlay()); entityTrackerLoadingProgress = p -> popup.progressBar.setProgress(p.floatValue()); Futures.addCallback(entityTrackerFuture, new FutureCallback() { @Override public void onSuccess(@Nullable Void result) { popup.close(); - updateKeyframe(isTime); + if (mod.getCurrentTimeline().getEntityTracker() == null) { + mod.getCurrentTimeline().setEntityTracker(entityTracker); + } + withDelayedTracker.run(); } @Override @@ -564,267 +558,63 @@ public class GuiPathing { popup.close(); } }); - return; + return false; } + if (mod.getCurrentTimeline().getEntityTracker() == null) { + mod.getCurrentTimeline().setEntityTracker(entityTracker); + } + return true; + } + + /** + * Called when either one of the property buttons is pressed. + * @param path {@code TIME} for the time property button, {@code POSITION} for the place property button + */ + private void updateKeyframe(SPPath path) { + LOGGER.debug("Updating keyframe on path {}" + path); + if (!ensureEntityTracker(() -> updateKeyframe(path))) return; int time = timeline.getCursorPosition(); - Timeline timeline = mod.getCurrentTimeline(); - Path path = timeline.getPaths().get(isTime ? TIME_PATH : POSITION_PATH); + SPTimeline timeline = mod.getCurrentTimeline(); - Keyframe keyframe = mod.getSelectedKeyframe(); - if (keyframe != null && keyframe.getValue(TimestampProperty.PROPERTY).isPresent() ^ isTime) { - // Keyframe is on the wrong timeline - keyframe = null; - } - if (keyframe == null) { - // No keyframe selected but there may still be one at this exact time - keyframe = path.getKeyframe(time); - } - Change change; - if (keyframe == null) { - change = AddKeyframe.create(path, time); - change.apply(timeline); - keyframe = path.getKeyframe(time); - } else { - change = RemoveKeyframe.create(path, keyframe); - change.apply(timeline); - keyframe = null; - } - - if (keyframe != null) { - UpdateKeyframeProperties.Builder builder = UpdateKeyframeProperties.create(path, keyframe); - if (isTime) { - builder.setValue(TimestampProperty.PROPERTY, replayHandler.getReplaySender().currentTimeStamp()); - } else { - CameraEntity camera = replayHandler.getCameraEntity(); - builder.setValue(CameraProperties.POSITION, Triple.of(camera.posX, camera.posY, camera.posZ)); - builder.setValue(CameraProperties.ROTATION, Triple.of(camera.rotationYaw, camera.rotationPitch, camera.roll)); - if (!replayHandler.isCameraView()) { - Entity spectated = replayHandler.getOverlay().getMinecraft().getRenderViewEntity(); - builder.setValue(SpectatorProperty.PROPERTY, spectated.getEntityId()); + switch (path) { + case TIME: + if (mod.getSelectedPath() == path) { + LOGGER.debug("Selected keyframe is time keyframe -> removing keyframe"); + timeline.removeTimeKeyframe(mod.getSelectedTime()); + mod.setSelected(null, 0); + } else if (timeline.isTimeKeyframe(time)) { + LOGGER.debug("Keyframe at cursor position is time keyframe -> removing keyframe"); + timeline.removeTimeKeyframe(time); + mod.setSelected(null, 0); + } else { + LOGGER.debug("No time keyframe found -> adding new keyframe"); + timeline.addTimeKeyframe(time, replayHandler.getReplaySender().currentTimeStamp()); + mod.setSelected(path, time); } - } - UpdateKeyframeProperties updateChange = builder.done(); - updateChange.apply(timeline); - change = CombinedChange.createFromApplied(change, updateChange); - - // If this new keyframe formed the first segment of the time path - if (isTime && path.getSegments().size() == 1) { - PathSegment segment = path.getSegments().iterator().next(); - Interpolator interpolator = new LinearInterpolator(); - interpolator.registerProperty(TimestampProperty.PROPERTY); - SetInterpolator setInterpolator = SetInterpolator.create(segment, interpolator); - setInterpolator.apply(timeline); - change = CombinedChange.createFromApplied(change, setInterpolator); - } - } - - // Update interpolators for spectator keyframes - // while this is overkill, it is far simpler than updating differently for every possible case - if (!isTime) { - Change interpolators = updateInterpolators(); - interpolators.apply(timeline); - change = CombinedChange.createFromApplied(change, interpolators); - } - - Change specPosUpdate = updateSpectatorPositions(); - specPosUpdate.apply(timeline); - change = CombinedChange.createFromApplied(change, specPosUpdate); - - timeline.pushChange(change); - - mod.setSelectedKeyframe(keyframe); - } - - public Change updateInterpolators() { - InterpolatorType type = InterpolatorType.fromString(mod.getCore().getSettingsRegistry().get(Setting.DEFAULT_INTERPOLATION)); - - Interpolator defaultInterpolator = null; - - // Iterate over all existing segments until the first interpolator - // of the same type without the fixed flag is found and clone it. - // This way, the default interpolator's settings can be saved on a per-camera-path basis. - // This is useful for interpolators that have customizable properties. - Collection pathSegments = mod.getCurrentTimeline().getPaths().get(POSITION_PATH).getSegments(); - - for (PathSegment segment : pathSegments) { - if (segment.getStartKeyframe().getValue(ExplicitInterpolationProperty.PROPERTY).isPresent()) continue; - if (segment.getInterpolator() == null) continue; - if (type == InterpolatorType.DEFAULT || type.getInterpolatorClass().equals(segment.getInterpolator().getClass())) { - try { - defaultInterpolator = mod.cloneInterpolator(segment.getInterpolator()); - break; - } catch (IOException e) { - Utils.error(LOGGER, replayHandler.getOverlay(), - CrashReport.makeCrashReport(e, "Cloning fixed interpolator"), null); - } - } - } - - // if no segment with the default interpolator was found, - // create a new one with default settings - if (defaultInterpolator == null) { - switch (type) { - case LINEAR: - defaultInterpolator = new LinearInterpolator(); - break; - case CUBIC: - default: - defaultInterpolator = new CubicSplineInterpolator(); - break; - } - } - - List changes = new ArrayList<>(); - Interpolator interpolator = null; - boolean isSpectatorInterpolator = false; - for (PathSegment segment : pathSegments) { - if (segment.getStartKeyframe().getValue(SpectatorProperty.PROPERTY).isPresent() - && segment.getEndKeyframe().getValue(SpectatorProperty.PROPERTY).isPresent()) { - // Spectator segment - if (!isSpectatorInterpolator) { - isSpectatorInterpolator = true; - interpolator = new LinearInterpolator(); - interpolator.registerProperty(SpectatorProperty.PROPERTY); - } - changes.add(SetInterpolator.create(segment, interpolator)); - } else { - // Normal segment - boolean explicit = segment.getStartKeyframe().getValue(ExplicitInterpolationProperty.PROPERTY).isPresent(); - if (isSpectatorInterpolator || interpolator == null || explicit) { - isSpectatorInterpolator = false; - - if (segment.getInterpolator() == null || !explicit) { - interpolator = defaultInterpolator; - interpolator.registerProperty(CameraProperties.POSITION); - interpolator.registerProperty(CameraProperties.ROTATION); - } else { - try { - interpolator = mod.cloneInterpolator(segment.getInterpolator()); - } catch (IOException e) { - Utils.error(LOGGER, replayHandler.getOverlay(), - CrashReport.makeCrashReport(e, "Cloning fixed interpolator"), null); - interpolator = defaultInterpolator; - } + break; + case POSITION: + if (mod.getSelectedPath() == path) { + LOGGER.debug("Selected keyframe is position keyframe -> removing keyframe"); + timeline.removePositionKeyframe(mod.getSelectedTime()); + mod.setSelected(null, 0); + } else if (timeline.isPositionKeyframe(time)) { + LOGGER.debug("Keyframe at cursor position is position keyframe -> removing keyframe"); + timeline.removePositionKeyframe(time); + mod.setSelected(null, 0); + } else { + LOGGER.debug("No position keyframe found -> adding new keyframe"); + CameraEntity camera = replayHandler.getCameraEntity(); + int spectatedId = -1; + if (!replayHandler.isCameraView()) { + spectatedId = replayHandler.getOverlay().getMinecraft().getRenderViewEntity().getEntityId(); } + timeline.addPositionKeyframe(time, camera.posX, camera.posY, camera.posZ, + camera.rotationYaw, camera.rotationPitch, camera.roll, spectatedId); + mod.setSelected(path, time); } - changes.add(SetInterpolator.create(segment, interpolator)); - } + break; } - return CombinedChange.create(changes.toArray(new Change[changes.size()])); - } - - public Change updateSpectatorPositions() { - List changes = new ArrayList<>(); - Path positionPath = mod.getCurrentTimeline().getPaths().get(POSITION_PATH); - Path timePath = mod.getCurrentTimeline().getPaths().get(TIME_PATH); - timePath.updateAll(); - for (Keyframe keyframe : positionPath.getKeyframes()) { - Optional spectator = keyframe.getValue(SpectatorProperty.PROPERTY); - if (spectator.isPresent()) { - Optional time = timePath.getValue(TimestampProperty.PROPERTY, keyframe.getTime()); - if (!time.isPresent()) { - continue; // No time keyframes set at this video time, cannot determine replay time - } - Location expected = entityTracker.getEntityPositionAtTimestamp(spectator.get(), time.get()); - if (expected == null) { - continue; // We don't have any data on this entity for some reason - } - Triple pos = keyframe.getValue(CameraProperties.POSITION).orElse(Triple.of(0D, 0D, 0D)); - Triple rot = keyframe.getValue(CameraProperties.ROTATION).orElse(Triple.of(0F, 0F, 0F)); - Location actual = new Location(pos.getLeft(), pos.getMiddle(), pos.getRight(), rot.getLeft(), rot.getRight()); - if (!expected.equals(actual)) { - changes.add(UpdateKeyframeProperties.create(positionPath, keyframe) - .setValue(CameraProperties.POSITION, Triple.of(expected.getX(), expected.getY(), expected.getZ())) - .setValue(CameraProperties.ROTATION, Triple.of(expected.getYaw(), expected.getPitch(), 0f)).done() - ); - } - } - } - return CombinedChange.create(changes.toArray(new Change[changes.size()])); - } - - public Change moveKeyframe(Path path, Keyframe keyframe, long newTime) { - Timeline timeline = mod.getCurrentTimeline(); - // InterpolatorType might be required later (only if path is the time path) - Optional firstInterpolator = - path.getSegments().stream().findFirst().map(PathSegment::getInterpolator); - - // The interpolator before the segment - Optional interpolatorBefore = - path.getSegments().stream().filter(s -> s.getEndKeyframe() == keyframe).findFirst().map(PathSegment::getInterpolator); - - // The interpolator that follows the segment - Optional interpolatorAfter = - path.getSegments().stream().filter(s -> s.getStartKeyframe() == keyframe).findFirst().map(PathSegment::getInterpolator); - - // First remove the old keyframe - Change removeChange = RemoveKeyframe.create(path, keyframe); - removeChange.apply(timeline); - - // and add a new one at the correct time - Change addChange = AddKeyframe.create(path, newTime); - addChange.apply(timeline); - Keyframe newKeyframe = path.getKeyframe(newTime); - - // Then copy over all properties - UpdateKeyframeProperties.Builder builder = UpdateKeyframeProperties.create(path, path.getKeyframe(newTime)); - for (Property property : keyframe.getProperties()) { - copyProperty(property, keyframe, builder); - } - Change propertyChange = builder.done(); - propertyChange.apply(timeline); - - // Set the interpolator of the segment before the keyframe to what it was - Change interpolatorBeforeChange; - - Optional segmentBefore = path.getSegments().stream().filter(s -> s.getEndKeyframe() == newKeyframe).findFirst(); - if (segmentBefore.isPresent() && interpolatorBefore.isPresent()) { - interpolatorBeforeChange = SetInterpolator.create(segmentBefore.get(), interpolatorBefore.get()); - } else { - interpolatorBeforeChange = CombinedChange.create(); - } - - interpolatorBeforeChange.apply(timeline); - - // Set the interpolator of the segment after the keyframe to what it was - Change interpolatorAfterChange; - - Optional segmentAfter = path.getSegments().stream().filter(s -> s.getStartKeyframe() == newKeyframe).findFirst(); - if (segmentAfter.isPresent() && interpolatorAfter.isPresent()) { - interpolatorAfterChange = SetInterpolator.create(segmentAfter.get(), interpolatorAfter.get()); - } else { - interpolatorAfterChange = CombinedChange.create(); - } - - interpolatorAfterChange.apply(timeline); - - // Finally update the interpolators - Change interpolatorUpdateChange; - if (path.getTimeline().getPaths().indexOf(path) == GuiPathing.POSITION_PATH) { - // Position / Spectator keyframes need special handling - interpolatorUpdateChange = updateInterpolators(); - } else { - // Time keyframes only need updating when only one segment of them exists - if (path.getSegments().size() == 1) { - interpolatorUpdateChange = SetInterpolator.create(path.getSegments().iterator().next(), firstInterpolator.get()); - } else { - interpolatorUpdateChange = CombinedChange.create(); // Noop change - } - } - - interpolatorUpdateChange.apply(timeline); - // and update spectator positions - Change spectatorChange = updateSpectatorPositions(); - spectatorChange.apply(timeline); - - return CombinedChange.createFromApplied(removeChange, addChange, propertyChange, - interpolatorBeforeChange, interpolatorAfterChange, interpolatorUpdateChange, spectatorChange); - } - - // Helper method because generics cannot be defined on blocks - private void copyProperty(Property property, Keyframe from, UpdateKeyframeProperties.Builder to) { - from.getValue(property).ifPresent(value -> to.setValue(property, value)); } public ReplayModSimplePathing getMod() { @@ -835,13 +625,15 @@ public class GuiPathing { return entityTracker; } - public void openEditKeyframePopup(Path path, Keyframe keyframe) { + public void openEditKeyframePopup(SPPath path, long time) { + if (!ensureEntityTracker(() -> openEditKeyframePopup(path, time))) return; + Keyframe keyframe = mod.getCurrentTimeline().getKeyframe(path, time); if (keyframe.getProperties().contains(SpectatorProperty.PROPERTY)) { - new GuiEditKeyframe.Spectator(this, path, keyframe).open(); + new GuiEditKeyframe.Spectator(this, path, keyframe.getTime()).open(); } else if (keyframe.getProperties().contains(CameraProperties.POSITION)) { - new GuiEditKeyframe.Position(this, path, keyframe).open(); + new GuiEditKeyframe.Position(this, path, keyframe.getTime()).open(); } else { - new GuiEditKeyframe.Time(this, path, keyframe).open(); + new GuiEditKeyframe.Time(this, path, keyframe.getTime()).open(); } } diff --git a/src/main/java/com/replaymod/simplepathing/preview/PathPreviewRenderer.java b/src/main/java/com/replaymod/simplepathing/preview/PathPreviewRenderer.java index 8c12a07b..101538ec 100644 --- a/src/main/java/com/replaymod/simplepathing/preview/PathPreviewRenderer.java +++ b/src/main/java/com/replaymod/simplepathing/preview/PathPreviewRenderer.java @@ -9,10 +9,10 @@ import com.replaymod.replaystudio.pathing.interpolation.Interpolator; import com.replaymod.replaystudio.pathing.path.Keyframe; import com.replaymod.replaystudio.pathing.path.Path; import com.replaymod.replaystudio.pathing.path.PathSegment; -import com.replaymod.replaystudio.pathing.path.Timeline; import com.replaymod.replaystudio.util.EntityPositionTracker; import com.replaymod.replaystudio.util.Location; import com.replaymod.simplepathing.ReplayModSimplePathing; +import com.replaymod.simplepathing.SPTimeline; import com.replaymod.simplepathing.gui.GuiPathing; import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.GlStateManager; @@ -67,11 +67,11 @@ public class PathPreviewRenderer { if (guiPathing == null) return; EntityPositionTracker entityTracker = guiPathing.getEntityTracker(); - Timeline timeline = mod.getCurrentTimeline(); + SPTimeline timeline = mod.getCurrentTimeline(); if (timeline == null) return; - Path path = timeline.getPaths().get(GuiPathing.POSITION_PATH); + Path path = timeline.getPositionPath(); if (path.getKeyframes().isEmpty()) return; - Path timePath = timeline.getPaths().get(GuiPathing.TIME_PATH); + Path timePath = timeline.getTimePath(); path.update(); @@ -230,7 +230,7 @@ public class PathPreviewRenderer { float posY = 0f; float size = 10f / ReplayMod.TEXTURE_SIZE; - if (keyframe == mod.getSelectedKeyframe()) { + if (mod.isSelected(keyframe)) { posY += size; } diff --git a/src/main/java/com/replaymod/pathing/properties/ExplicitInterpolationProperty.java b/src/main/java/com/replaymod/simplepathing/properties/ExplicitInterpolationProperty.java similarity index 85% rename from src/main/java/com/replaymod/pathing/properties/ExplicitInterpolationProperty.java rename to src/main/java/com/replaymod/simplepathing/properties/ExplicitInterpolationProperty.java index 6443e91b..5f181191 100644 --- a/src/main/java/com/replaymod/pathing/properties/ExplicitInterpolationProperty.java +++ b/src/main/java/com/replaymod/simplepathing/properties/ExplicitInterpolationProperty.java @@ -1,10 +1,11 @@ -package com.replaymod.pathing.properties; +package com.replaymod.simplepathing.properties; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; import com.replaymod.replaystudio.pathing.property.AbstractProperty; import com.replaymod.replaystudio.pathing.property.PropertyPart; import lombok.NonNull; +import org.apache.commons.lang3.ObjectUtils; import java.io.IOException; import java.util.Collection; @@ -36,8 +37,8 @@ public class ExplicitInterpolationProperty extends AbstractProperty { } @Override - public Void fromJson(JsonReader reader) throws IOException { + public Object fromJson(JsonReader reader) throws IOException { reader.nextNull(); - return null; + return ObjectUtils.NULL; } } diff --git a/src/test/java/com/replaymod/simplepathing/SPTimelineTest.java b/src/test/java/com/replaymod/simplepathing/SPTimelineTest.java new file mode 100644 index 00000000..b94c7a53 --- /dev/null +++ b/src/test/java/com/replaymod/simplepathing/SPTimelineTest.java @@ -0,0 +1,594 @@ +package com.replaymod.simplepathing; + +import com.google.common.collect.Iterables; +import com.replaymod.pathing.properties.CameraProperties; +import com.replaymod.pathing.properties.SpectatorProperty; +import com.replaymod.pathing.properties.TimestampProperty; +import com.replaymod.replaystudio.pathing.change.Change; +import com.replaymod.replaystudio.pathing.interpolation.Interpolator; +import com.replaymod.replaystudio.pathing.interpolation.LinearInterpolator; +import com.replaymod.replaystudio.pathing.path.Keyframe; +import com.replaymod.replaystudio.pathing.path.Path; +import com.replaymod.replaystudio.pathing.path.PathSegment; +import com.replaymod.replaystudio.pathing.path.Timeline; +import com.replaymod.replaystudio.util.EntityPositionTracker; +import com.replaymod.replaystudio.util.Location; +import com.replaymod.simplepathing.SPTimeline.SPPath; +import org.apache.commons.lang3.tuple.Triple; +import org.apache.logging.log4j.LogManager; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.Collections; +import java.util.Comparator; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +public class SPTimelineTest { + private SPTimeline impl; + private Timeline timeline; + + @BeforeClass + public static void setupLogger() { + ReplayModSimplePathing.LOGGER = LogManager.getLogger(); + } + + @Before + public void setup() { + impl = new SPTimeline(); + impl.setDefaultInterpolatorType(InterpolatorType.CUBIC); + impl.setEntityTracker(new EntityPositionTrackerMock()); + timeline = impl.getTimeline(); + } + + @Test + public void testGetPath() { + assertSame(impl.getTimeline().getPaths().get(0), impl.getTimePath()); + assertSame(impl.getTimeline().getPaths().get(0), impl.getPath(SPPath.TIME)); + assertSame(impl.getTimeline().getPaths().get(1), impl.getPositionPath()); + assertSame(impl.getTimeline().getPaths().get(1), impl.getPath(SPPath.POSITION)); + } + + @Test + public void testGetKeyframe() { + assertSame(timeline.getPaths().get(0).insert(123), impl.getKeyframe(SPPath.TIME, 123)); + assertSame(timeline.getPaths().get(1).insert(456), impl.getKeyframe(SPPath.POSITION, 456)); + } + + @Test + public void testIsTimeKeyframe() { + impl.getTimePath().insert(123); + impl.getPositionPath().insert(456); + impl.getPositionPath().insert(42).setValue(SpectatorProperty.PROPERTY, 42); + assertTrue(impl.isTimeKeyframe(123)); + assertFalse(impl.isTimeKeyframe(456)); + assertFalse(impl.isTimeKeyframe(789)); + assertFalse(impl.isTimeKeyframe(42)); + } + + @Test + public void testIsPositionKeyframe() { + impl.getTimePath().insert(123); + impl.getPositionPath().insert(456); + impl.getPositionPath().insert(42).setValue(SpectatorProperty.PROPERTY, 42); + assertFalse(impl.isPositionKeyframe(123)); + assertTrue(impl.isPositionKeyframe(456)); + assertFalse(impl.isPositionKeyframe(789)); + assertTrue(impl.isSpectatorKeyframe(42)); + } + + @Test + public void testIsSpectatorKeyframe() { + impl.getTimePath().insert(123); + impl.getPositionPath().insert(456); + impl.getPositionPath().insert(42).setValue(SpectatorProperty.PROPERTY, 42); + assertFalse(impl.isSpectatorKeyframe(123)); + assertFalse(impl.isSpectatorKeyframe(456)); + assertFalse(impl.isSpectatorKeyframe(789)); + assertTrue(impl.isSpectatorKeyframe(42)); + } + + @Test + public void testAddPositionKeyframe() { + assertNull(timeline.peekUndoStack()); + impl.addPositionKeyframe(0, 1, 2, 3, 4, 5, 6, -1); + assertNotNull(timeline.peekUndoStack()); + Keyframe keyframe = impl.getKeyframe(SPPath.POSITION, 0); + assertNotNull(keyframe); + assertEquals(keyframe.getValue(CameraProperties.POSITION), Optional.of(Triple.of(1d, 2d, 3d))); + assertEquals(keyframe.getValue(CameraProperties.ROTATION), Optional.of(Triple.of(4f, 5f, 6f))); + assertFalse(keyframe.getValue(SpectatorProperty.PROPERTY).isPresent()); + timeline.undoLastChange(); + assertNull(timeline.peekUndoStack()); + assertNull(impl.getKeyframe(SPPath.POSITION, 0)); + } + + @Test + public void testAddPositionKeyframeSpectator() { + assertNull(timeline.peekUndoStack()); + impl.addPositionKeyframe(0, 1, 2, 3, 4, 5, 6, 7); + assertNotNull(timeline.peekUndoStack()); + Keyframe keyframe = impl.getKeyframe(SPPath.POSITION, 0); + assertNotNull(keyframe); + assertEquals(keyframe.getValue(CameraProperties.POSITION), Optional.of(Triple.of(1d, 2d, 3d))); + assertEquals(keyframe.getValue(CameraProperties.ROTATION), Optional.of(Triple.of(4f, 5f, 6f))); + assertEquals(keyframe.getValue(SpectatorProperty.PROPERTY), Optional.of(7)); + timeline.undoLastChange(); + assertNull(timeline.peekUndoStack()); + assertNull(impl.getKeyframe(SPPath.POSITION, 0)); + } + + @Test + public void testPositionKeyframeSpectatorPosition() { + Keyframe keyframe; + impl.addTimeKeyframe(0, 0); + impl.addTimeKeyframe(9, 9); + + impl.addPositionKeyframe(4, 1, 2, 3, 4, 5, 6, 1); + keyframe = impl.getKeyframe(SPPath.POSITION, 4); + assertEquals(keyframe.getValue(CameraProperties.POSITION), Optional.of(Triple.of(1d, 2d, 3d))); + assertEquals(keyframe.getValue(CameraProperties.ROTATION), Optional.of(Triple.of(4f, 5f, 6f))); + + impl.addPositionKeyframe(6, 1, 2, 3, 4, 5, 6, 42); + keyframe = impl.getKeyframe(SPPath.POSITION, 6); + assertEquals(keyframe.getValue(CameraProperties.POSITION), Optional.of(Triple.of(12d, 0d, 0d))); + assertEquals(keyframe.getValue(CameraProperties.ROTATION), Optional.of(Triple.of(0f, 0f, 0f))); + + impl.addPositionKeyframe(8, 1, 2, 3, 4, 5, 6, 42); + keyframe = impl.getKeyframe(SPPath.POSITION, 8); + assertEquals(keyframe.getValue(CameraProperties.POSITION), Optional.of(Triple.of(16d, 0d, 0d))); + assertEquals(keyframe.getValue(CameraProperties.ROTATION), Optional.of(Triple.of(0f, 0f, 0f))); + + impl.addPositionKeyframe(10, 1, 2, 3, 4, 5, 6, 42); + keyframe = impl.getKeyframe(SPPath.POSITION, 10); + assertEquals(keyframe.getValue(CameraProperties.POSITION), Optional.of(Triple.of(1d, 2d, 3d))); + assertEquals(keyframe.getValue(CameraProperties.ROTATION), Optional.of(Triple.of(4f, 5f, 6f))); + } + + @Test(expected = IllegalStateException.class) + public void testAddPositionKeyframeDuplicate() { + impl.addPositionKeyframe(0, 1, 2, 3, 4, 5, 6, -1); + impl.addPositionKeyframe(0, 1, 2, 3, 4, 5, 6, -1); + } + + @Test + public void testUpdatePositionKeyframe() { + impl.addPositionKeyframe(0, 1, 2, 3, 4, 5, 6, -1); + Change change = impl.updatePositionKeyframe(0, 7, 8, 9, 10, 11, 12); + assertNotNull(change); + Keyframe keyframe = impl.getKeyframe(SPPath.POSITION, 0); + assertEquals(keyframe.getValue(CameraProperties.POSITION), Optional.of(Triple.of(7d, 8d, 9d))); + assertEquals(keyframe.getValue(CameraProperties.ROTATION), Optional.of(Triple.of(10f, 11f, 12f))); + assertFalse(keyframe.getValue(SpectatorProperty.PROPERTY).isPresent()); + change.undo(timeline); + keyframe = impl.getKeyframe(SPPath.POSITION, 0); + assertEquals(keyframe.getValue(CameraProperties.POSITION), Optional.of(Triple.of(1d, 2d, 3d))); + assertEquals(keyframe.getValue(CameraProperties.ROTATION), Optional.of(Triple.of(4f, 5f, 6f))); + assertFalse(keyframe.getValue(SpectatorProperty.PROPERTY).isPresent()); + } + + @Test(expected = IllegalStateException.class) + public void testUpdatePositionKeyframeNoKeyframe() { + impl.updatePositionKeyframe(0, 1, 2, 3, 4, 5, 6); + } + + @Test(expected = IllegalStateException.class) + public void testUpdatePositionKeyframeSpectatorKeyframe() { + impl.addPositionKeyframe(0, 1, 2, 3, 4, 5, 6, 7); + impl.updatePositionKeyframe(0, 1, 2, 3, 4, 5, 6); + } + + @Test + public void testRemovePositionKeyframe() { + impl.addPositionKeyframe(0, 1, 2, 3, 4, 5, 6, -1); + impl.removePositionKeyframe(0); + assertNull(impl.getKeyframe(SPPath.POSITION, 0)); + } + + @Test(expected = IllegalStateException.class) + public void testRemovePositionKeyframeNoKeyframe() { + impl.removePositionKeyframe(0); + } + + @Test + public void testAddTimeKeyframe() { + assertNull(timeline.peekUndoStack()); + impl.addTimeKeyframe(0, 1); + assertNotNull(timeline.peekUndoStack()); + Keyframe keyframe = impl.getKeyframe(SPPath.TIME, 0); + assertNotNull(keyframe); + assertEquals(keyframe.getValue(TimestampProperty.PROPERTY), Optional.of(1)); + timeline.undoLastChange(); + assertNull(timeline.peekUndoStack()); + assertNull(impl.getKeyframe(SPPath.TIME, 0)); + } + + @Test(expected = IllegalStateException.class) + public void testAddTimeKeyframeDuplicate() { + impl.addTimeKeyframe(0, 1); + impl.addTimeKeyframe(0, 1); + } + + @Test + public void testUpdateTimeKeyframe() { + impl.addTimeKeyframe(0, 1); + Change change = impl.updateTimeKeyframe(0, 2); + assertNotNull(change); + assertEquals(impl.getKeyframe(SPPath.TIME, 0).getValue(TimestampProperty.PROPERTY), Optional.of(2)); + change.undo(timeline); + assertEquals(impl.getKeyframe(SPPath.TIME, 0).getValue(TimestampProperty.PROPERTY), Optional.of(1)); + } + + @Test(expected = IllegalStateException.class) + public void testUpdateTimeKeyframeNoKeyframe() { + impl.updateTimeKeyframe(0, 1); + } + + @Test + public void testRemoveTimeKeyframe() { + impl.addTimeKeyframe(0, 1); + impl.removeTimeKeyframe(0); + assertNull(impl.getKeyframe(SPPath.TIME, 0)); + } + + @Test(expected = IllegalStateException.class) + public void testRemoveTimeKeyframeNoKeyframe() { + impl.removeTimeKeyframe(0); + } + + /** + * Checks all interpolators to make sure that they are continuous and not fragmented. + */ + private void assertValidInterpolators(SPPath path, int expectedNumberOfInterpolators) { + String str = prettyPrintInterpolators(impl, path); + List interpolators = impl.getPath(path).getSegments().stream().map(PathSegment::getInterpolator) + .distinct().collect(Collectors.toList()); + assertFalse("Not all interpolators set: " + str, interpolators.contains(null)); + assertEquals("Mismatched interpolator number: " + str, expectedNumberOfInterpolators, interpolators.size()); + int i = 0; + for (Interpolator interpolator : interpolators) { + Keyframe lastKeyframe = null; + for (PathSegment segment : interpolator.getSegments().stream() + .sorted(Comparator.comparing(s -> s.getStartKeyframe().getTime())).collect(Collectors.toList())) { + String message = "Fragmentation in interpolator " + i + ": " + str; + assertTrue(message, lastKeyframe == null || segment.getStartKeyframe() == lastKeyframe); + lastKeyframe = segment.getEndKeyframe(); + } + i++; + } + } + + @Test + public void testAddPositionKeyframeAppend() { + addPosition(0, 0); + addPosition(1, 1); + addPosition(2, 1); + addPosition(3, 1); + addPosition(4, 1); + } + + @Test + public void testAddPositionKeyframePrepend() { + addPosition(4, 0); + addPosition(3, 1); + addPosition(2, 1); + addPosition(1, 1); + addPosition(0, 1); + } + + @Test + public void testAddPositionKeyframeMixed() { + addPosition(2, 0); + addPosition(4, 1); + addPosition(3, 1); + addPosition(0, 1); + addPosition(1, 1); + } + + @Test + public void testAddPositionSpectatorKeyframe() { + addPosition(0, 0); + addSpectator(1, 1); + addSpectator(2, 2); + addPosition(4, 3); + addPosition(7, 3); + addPosition(8, 3); + addSpectator(3, 3); + addSpectator(5, 3); + addSpectator(6, 5); + } + + @Test + public void testRemovePositionKeyframeFromStart() { + addPosition(0, 0); + addPosition(1, 1); + addPosition(2, 1); + addPosition(3, 1); + removePosition(0, 1); + removePosition(1, 1); + removePosition(2, 0); + removePosition(3, 0); + } + + @Test + public void testRemovePositionKeyframeFromEnd() { + addPosition(0, 0); + addPosition(1, 1); + addPosition(2, 1); + addPosition(3, 1); + removePosition(3, 1); + removePosition(2, 1); + removePosition(1, 0); + removePosition(0, 0); + } + + @Test + public void testRemovePositionKeyframeFromMiddle() { + addPosition(0, 0); + addPosition(1, 1); + addPosition(2, 1); + addPosition(3, 1); + removePosition(1, 1); + removePosition(2, 1); + removePosition(0, 0); + removePosition(3, 0); + } + + @Test + public void testRemovePositionSpectatorKeyframe() { + addPosition(0, 0); + addPosition(1, 1); + addPosition(2, 1); + addSpectator(3, 1); + addSpectator(4, 2); + addSpectator(5, 2); + addPosition(6, 3); + addPosition(7, 3); + addSpectator(8, 3); + addSpectator(9, 4); + removePosition(4, 4); + removePosition(9, 3); + removePosition(6, 3); + removePosition(7, 2); + removePosition(0, 2); + removePosition(1, 2); + removePosition(3, 2); + removePosition(2, 1); + removePosition(5, 0); + removePosition(8, 0); + } + + @Test + public void testAddTimeKeyframeAppend() { + addTime(0, 0); + addTime(1, 1); + addTime(2, 1); + addTime(3, 1); + addTime(4, 1); + } + + @Test + public void testAddTimeKeyframePrepend() { + addTime(4, 0); + addTime(3, 1); + addTime(2, 1); + addTime(1, 1); + addTime(0, 1); + } + + @Test + public void testAddTimeKeyframeMixed() { + addTime(2, 0); + addTime(4, 1); + addTime(3, 1); + addTime(0, 1); + addTime(1, 1); + } + + @Test + public void testRemoveTimeKeyframeFromStart() { + addTime(0, 0); + addTime(1, 1); + addTime(2, 1); + addTime(3, 1); + removeTime(0, 1); + removeTime(1, 1); + removeTime(2, 0); + removeTime(3, 0); + } + + @Test + public void testRemoveTimeKeyframeFromEnd() { + addTime(0, 0); + addTime(1, 1); + addTime(2, 1); + addTime(3, 1); + removeTime(3, 1); + removeTime(2, 1); + removeTime(1, 0); + removeTime(0, 0); + } + + @Test + public void testRemoveTimeKeyframeFromMiddle() { + addTime(0, 0); + addTime(1, 1); + addTime(2, 1); + addTime(3, 1); + removeTime(1, 1); + removeTime(2, 1); + removeTime(0, 0); + removeTime(3, 0); + } + + @Test + public void testSetInterpolator() { + addPosition(0, 0); + addPosition(1, 1); + addPosition(2, 1); + setInterpolator(1, new LinearInterpolator(), 2); + assertIsLinear(1); + Interpolator interpolator = Iterables.get(impl.getPositionPath().getSegments(), 1).getInterpolator(); + assertTrue(interpolator.getKeyframeProperties().contains(CameraProperties.POSITION)); + assertTrue(interpolator.getKeyframeProperties().contains(CameraProperties.ROTATION)); + addPosition(3, 3); + addPosition(4, 3); + addPosition(5, 3); + setInterpolator(3, new LinearInterpolator(), 5); + assertIsLinear(3); + removePosition(2, 3); + removePosition(3, 3); + removePosition(0, 2); + removePosition(1, 1); + removePosition(4, 0); + removePosition(5, 0); + } + + private void assertIsLinear(int index) { + assertTrue("Expected segment " + index + " to have linear interpolator: " + prettyPrintInterpolators(impl, SPPath.POSITION), + Iterables.get(impl.getPositionPath().getSegments(), index).getInterpolator() instanceof LinearInterpolator); + } + + @Test(expected = IllegalStateException.class) + public void testSetInterpolatorNoKeyframe() { + addTime(0, 0); + impl.setInterpolator(0, new LinearInterpolator()); + } + + @Test(expected = IllegalStateException.class) + public void testSetInterpolatorToDefaultNoKeyframe() { + addTime(0, 0); + impl.setInterpolatorToDefault(0); + } + + @Test + public void testMoveKeyframe() { + addPosition(1, 0); + addPosition(3, 1); + addPosition(5, 1); + addPosition(7, 1); + addSpectator(9, 1); + addSpectator(11, 2); + addSpectator(13, 2); + addPosition(15, 3); + addPosition(17, 3); + addPosition(19, 3); + setInterpolator(3, new LinearInterpolator(), 5); + assertIsLinear(1); + setInterpolator(5, new LinearInterpolator(), 5); + assertIsLinear(2); + // [P 1] C0 [P 3] L1 [P 5] L1 [P 7] C2 [S 9] L3 [S 11] L3 [S 13] C4 [P 15] C4 [P 17] C4 [P 19] + + impl.moveKeyframe(SPPath.POSITION, 5, 0); + assertValidInterpolators(SPPath.POSITION, 6); + assertIsLinear(0); + impl.moveKeyframe(SPPath.POSITION, 0, 5); + assertValidInterpolators(SPPath.POSITION, 5); + assertIsLinear(2); + + impl.moveKeyframe(SPPath.POSITION, 3, 12); + assertValidInterpolators(SPPath.POSITION, 7); + assertIsLinear(5); + impl.moveKeyframe(SPPath.POSITION, 12, 3); + assertValidInterpolators(SPPath.POSITION, 5); + assertIsLinear(1); + + impl.moveKeyframe(SPPath.POSITION, 7, 14); + assertValidInterpolators(SPPath.POSITION, 4); + impl.moveKeyframe(SPPath.POSITION, 14, 20); + assertValidInterpolators(SPPath.POSITION, 4); + } + + private void addPosition(int time, int expectedNumberOfInterpolators) { + impl.addPositionKeyframe(time, 1, 2, 3, 4, 5, 6, -1); + assertNotNull(impl.getKeyframe(SPPath.POSITION, time)); + assertValidInterpolators(SPPath.POSITION, expectedNumberOfInterpolators); + } + + private void addSpectator(int time, int expectedNumberOfInterpolators) { + impl.addPositionKeyframe(time, 1, 2, 3, 4, 5, 6, 42); + assertNotNull(impl.getKeyframe(SPPath.POSITION, time)); + assertTrue(impl.isSpectatorKeyframe(time)); + assertValidInterpolators(SPPath.POSITION, expectedNumberOfInterpolators); + } + + private void addTime(int time, int expectedNumberOfInterpolators) { + impl.addTimeKeyframe(time, 1); + assertNotNull(impl.getKeyframe(SPPath.TIME, time)); + assertValidInterpolators(SPPath.TIME, expectedNumberOfInterpolators); + } + + private void removePosition(int time, int expectedNumberOfInterpolators) { + impl.removePositionKeyframe(time); + assertNull(impl.getKeyframe(SPPath.POSITION, time)); + assertValidInterpolators(SPPath.POSITION, expectedNumberOfInterpolators); + } + + private void removeTime(int time, int expectedNumberOfInterpolators) { + impl.removeTimeKeyframe(time); + assertNull(impl.getKeyframe(SPPath.TIME, time)); + assertValidInterpolators(SPPath.TIME, expectedNumberOfInterpolators); + } + + private void setInterpolator(int time, Interpolator interpolator, int expectedNumberOfInterpolators) { + impl.setInterpolator(time, interpolator); + assertValidInterpolators(SPPath.POSITION, expectedNumberOfInterpolators); + } + + private void assertPositionState(String expected) { + assertEquals(expected, prettyPrintInterpolators(impl, SPPath.POSITION)); + } + + // Tracks only entity 42 which always is at x=time*2, y,z=0 + private class EntityPositionTrackerMock extends EntityPositionTracker { + public EntityPositionTrackerMock() { + super(null); + } + + @Override + public Location getEntityPositionAtTimestamp(int entityID, long timestamp) { + if (entityID == 42) { + return new Location(timestamp * 2, 0 ,0, 0, 0); + } else { + return null; + } + } + } + + public static String prettyPrintInterpolators(SPTimeline timeline, SPPath spPath) { + return prettyPrintInterpolators(timeline, spPath, Collections.emptyMap()); + } + + public static String prettyPrintInterpolators(SPTimeline timeline, SPPath spPath, Map updates) { + Map interpolatorIdMap = new IdentityHashMap<>(); + Path path = timeline.getPath(spPath); + StringBuilder sb = new StringBuilder(); + for (Keyframe keyframe : path.getKeyframes()) { + if (spPath == SPPath.TIME) { + sb.append("[T ").append(keyframe.getTime()).append("]"); + } else if (keyframe.getValue(SpectatorProperty.PROPERTY).isPresent()) { + sb.append("[S ").append(keyframe.getTime()).append("]"); + } else { + sb.append("[P ").append(keyframe.getTime()).append("]"); + } + path.getSegments().stream().filter(s -> s.getStartKeyframe() == keyframe).findFirst().ifPresent(segment -> { + Interpolator interpolator = updates.getOrDefault(segment, segment.getInterpolator()); + String id = interpolatorIdMap.computeIfAbsent(interpolator, i -> + (i instanceof LinearInterpolator ? "L" : "C") + interpolatorIdMap.size()); + sb.append(' ').append(id).append(' '); + }); + } + return sb.toString(); + } +} From d2def94fc17b0046ad028324f3560bb5794b28fc Mon Sep 17 00:00:00 2001 From: johni0702 Date: Mon, 23 Jan 2017 17:52:11 +0100 Subject: [PATCH 06/19] Fix duration of replays corrupted because of JVM/OS crash A JVM or OS crash can lead to incomplete packets at the end of a recording. The duration of a corrupted replay file is not restored properly if that file contains an incomplete packet. Furthermore, the entity tracker does not load properly with such an incomplete packet. Both bugs are fixed with this commit. To fix these bugs for replays that have already been recovered, simply open them in the Replay Viewer, modify them somehow (save a new path) and force the game to quit forcefully. The replays will be recovered the next time the game starts and both bugs will be fixed. --- .../java/com/replaymod/core/gui/RestoreReplayGui.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/replaymod/core/gui/RestoreReplayGui.java b/src/main/java/com/replaymod/core/gui/RestoreReplayGui.java index c6a4a3d1..d922c1e4 100644 --- a/src/main/java/com/replaymod/core/gui/RestoreReplayGui.java +++ b/src/main/java/com/replaymod/core/gui/RestoreReplayGui.java @@ -3,6 +3,7 @@ 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.io.ReplayOutputStream; import com.replaymod.replaystudio.replay.ReplayFile; import com.replaymod.replaystudio.replay.ReplayMetaData; import com.replaymod.replaystudio.replay.ZipReplayFile; @@ -45,15 +46,19 @@ public class RestoreReplayGui extends AbstractGuiScreen { ReplayMetaData metaData = replayFile.getMetaData(); if (metaData != null && metaData.getDuration() == 0) { // Try to restore replay duration - try (ReplayInputStream in = replayFile.getPacketData()) { + // We need to re-write the packet data in case there are any incomplete packets dangling at the end + try (ReplayInputStream in = replayFile.getPacketData(); + ReplayOutputStream out = replayFile.writePacketData()) { PacketData last = null; while ((last = in.readPacket()) != null) { metaData.setDuration((int) last.getTime()); + out.write(last); } - replayFile.writeMetaData(metaData); } catch (Throwable t) { t.printStackTrace(); } + // Write back the actual duration + replayFile.writeMetaData(metaData); } replayFile.save(); replayFile.close(); From 3f2b3d62f7000bb7697954814c2049701dce729c Mon Sep 17 00:00:00 2001 From: johni0702 Date: Tue, 24 Jan 2017 09:39:12 +0100 Subject: [PATCH 07/19] Fix replacing source file in Replay Editor (fixes #57) --- .../replaymod/editor/gui/GuiReplayEditor.java | 35 +++++++++++++++---- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/replaymod/editor/gui/GuiReplayEditor.java b/src/main/java/com/replaymod/editor/gui/GuiReplayEditor.java index 98ef0760..62b027ab 100644 --- a/src/main/java/com/replaymod/editor/gui/GuiReplayEditor.java +++ b/src/main/java/com/replaymod/editor/gui/GuiReplayEditor.java @@ -1,5 +1,6 @@ package com.replaymod.editor.gui; +import com.google.common.io.Files; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.gson.JsonObject; @@ -34,6 +35,7 @@ import de.johni0702.minecraft.gui.popup.GuiInfoPopup; import de.johni0702.minecraft.gui.popup.GuiYesNoPopup; import de.johni0702.minecraft.gui.utils.Colors; import net.minecraft.crash.CrashReport; +import org.apache.commons.io.FileUtils; import org.apache.commons.io.FilenameUtils; import org.lwjgl.util.ReadableDimension; @@ -119,10 +121,21 @@ public class GuiReplayEditor extends GuiScreen { public void save(File inputFile, PacketStream.FilterInfo...filters) { save(FilenameUtils.getBaseName(inputFile.getName()), (outputFile) -> { Studio studio = new ReplayStudio(); - try (ReplayFile outputReplay = new ZipReplayFile(studio, inputFile, outputFile); - ReplayOutputStream out = outputReplay.writePacketData()) { - // The input replay file MUST be closed before saving the output file - try (ReplayFile inputReplay = new ZipReplayFile(studio, inputFile); + File tmpDir = null; + try { + File actualOutputFile = outputFile; + if (outputFile.getCanonicalPath().equals(inputFile.getCanonicalPath())) { + // Input and output files are identical. Due to the way the ZipReplayFile stores its temporary + // data, the same replay file must not be opened twice for writing (tmp files will be deleted once + // either one is closed or, on Windows, will throw an exception when deleted). + tmpDir = Files.createTempDir(); + outputFile = new File(tmpDir, "replay.mcpr"); + LOGGER.debug("Output file is identical to input file, using temporary output file {} instead", + outputFile); + } + try (ReplayFile outputReplay = new ZipReplayFile(studio, inputFile, outputFile); + ReplayOutputStream out = outputReplay.writePacketData(); + ReplayFile inputReplay = new ZipReplayFile(studio, inputFile); ReplayInputStream in = inputReplay.getPacketData()) { ReplayMetaData metaData = inputReplay.getMetaData(); PacketStream stream = studio.createReplayStream(in, true); @@ -154,11 +167,21 @@ public class GuiReplayEditor extends GuiScreen { // Update duration of new replay metaData.setDuration((int) lastTimestamp); outputReplay.writeMetaData(metaData); + + out.close(); + outputReplay.save(); + } + if (outputFile != actualOutputFile) { + LOGGER.debug("Moving temporary output file {} to {}"); + FileUtils.forceDelete(actualOutputFile); + FileUtils.moveFile(outputFile, actualOutputFile); } - out.close(); - outputReplay.save(); } catch (IOException e) { throw new RuntimeException(e); + } finally { + if (tmpDir != null && !FileUtils.deleteQuietly(tmpDir)) { + LOGGER.warn("Failed to delete temporary directory {}", tmpDir); + } } }); } From 70e3e5403885f4e885df45978cdc9fadcd9558d3 Mon Sep 17 00:00:00 2001 From: johni0702 Date: Tue, 24 Jan 2017 13:07:50 +0100 Subject: [PATCH 08/19] Fix AbstractMethodError caused by FG2.0 not supporting lambdas (fixes #58) Lambdas are not properly re-obfuscated prior to FG2.2 causing said errors when used. This is the only instance of such a lambda. FG2.2 only supports MC 1.8.8 and up as such the lambda was replaced with an anonymous inner class to solve the problem. --- .../packet/ResourcePackRecorder.java | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/replaymod/recording/packet/ResourcePackRecorder.java b/src/main/java/com/replaymod/recording/packet/ResourcePackRecorder.java index a00ff274..d4cb6c0f 100644 --- a/src/main/java/com/replaymod/recording/packet/ResourcePackRecorder.java +++ b/src/main/java/com/replaymod/recording/packet/ResourcePackRecorder.java @@ -9,6 +9,7 @@ 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.gui.GuiYesNoCallback; import net.minecraft.client.multiplayer.ServerData; import net.minecraft.client.multiplayer.ServerList; import net.minecraft.client.network.NetHandlerPlayClient; @@ -107,19 +108,24 @@ public class ResourcePackRecorder { } 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)); - } + // Lambdas MUST NOT be used with methods that need re-obfuscation in FG prior to 2.2 (will result in AbstractMethodError) + //noinspection Convert2Lambda + mc.addScheduledTask(() -> 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)); + ResourcePackRecorder.this.downloadResourcePackFuture(requestId, url, hash); + } else { + netManager.sendPacket(new C19PacketResourcePackStatus(hash, C19PacketResourcePackStatus.Action.DECLINED)); + } - ServerList.func_147414_b(serverData); - mc.displayGuiScreen(null); + ServerList.func_147414_b(serverData); + mc.displayGuiScreen(null); + } }, I18n.format("multiplayer.texturePrompt.line1"), I18n.format("multiplayer.texturePrompt.line2"), 0))); } } From 64898ce288f5fcc54ebb4304a9b1f0aad76413bf Mon Sep 17 00:00:00 2001 From: johni0702 Date: Tue, 24 Jan 2017 13:08:55 +0100 Subject: [PATCH 09/19] Fix NPE when saving recorded resource pack --- .../com/replaymod/recording/packet/ResourcePackRecorder.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/com/replaymod/recording/packet/ResourcePackRecorder.java b/src/main/java/com/replaymod/recording/packet/ResourcePackRecorder.java index d4cb6c0f..ed5cfc7f 100644 --- a/src/main/java/com/replaymod/recording/packet/ResourcePackRecorder.java +++ b/src/main/java/com/replaymod/recording/packet/ResourcePackRecorder.java @@ -27,6 +27,7 @@ import javax.annotation.Nonnull; import java.io.File; import java.io.IOException; import java.io.OutputStream; +import java.util.HashMap; import java.util.Map; /** @@ -53,6 +54,9 @@ public class ResourcePackRecorder { 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 index = replayFile.getResourcePackIndex(); + if (index == null) { + index = new HashMap<>(); + } if (!index.containsValue(hash)) { // Hash is unknown, we have to write the resource pack ourselves doWrite = true; From c6293bcbb0bbea51d5621b8bce5e4d5b81c383d4 Mon Sep 17 00:00:00 2001 From: johni0702 Date: Tue, 24 Jan 2017 13:27:57 +0100 Subject: [PATCH 10/19] Update to FG2.2 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index fd4a9309..540972d6 100755 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ buildscript { } } dependencies { - classpath 'net.minecraftforge.gradle:ForgeGradle:2.1-SNAPSHOT' + classpath 'net.minecraftforge.gradle:ForgeGradle:2.2-SNAPSHOT' classpath 'org.spongepowered:mixingradle:0.3-SNAPSHOT' } } From 3f52336852b9f9efd0fcba1827ee8fcc8777b671 Mon Sep 17 00:00:00 2001 From: johni0702 Date: Tue, 24 Jan 2017 13:29:09 +0100 Subject: [PATCH 11/19] Revert "Fix AbstractMethodError caused by FG2.0 not supporting lambdas (fixes #58)" This reverts commit 70e3e5403885f4e885df45978cdc9fadcd9558d3. As of c6293bc, this branch uses FG2.2 which fully supports lambdas. --- .../packet/ResourcePackRecorder.java | 30 ++++++++----------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/replaymod/recording/packet/ResourcePackRecorder.java b/src/main/java/com/replaymod/recording/packet/ResourcePackRecorder.java index cec0ebfa..30e421c7 100644 --- a/src/main/java/com/replaymod/recording/packet/ResourcePackRecorder.java +++ b/src/main/java/com/replaymod/recording/packet/ResourcePackRecorder.java @@ -9,7 +9,6 @@ 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.gui.GuiYesNoCallback; import net.minecraft.client.multiplayer.ServerData; import net.minecraft.client.multiplayer.ServerList; import net.minecraft.client.network.NetHandlerPlayClient; @@ -112,24 +111,19 @@ public class ResourcePackRecorder { } else if (serverData != null && serverData.getResourceMode() != ServerData.ServerResourceMode.PROMPT) { netManager.sendPacket(new C19PacketResourcePackStatus(hash, C19PacketResourcePackStatus.Action.DECLINED)); } else { - // Lambdas MUST NOT be used with methods that need re-obfuscation in FG prior to 2.2 (will result in AbstractMethodError) - //noinspection Convert2Lambda - mc.addScheduledTask(() -> 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)); - ResourcePackRecorder.this.downloadResourcePackFuture(requestId, url, hash); - } else { - netManager.sendPacket(new C19PacketResourcePackStatus(hash, C19PacketResourcePackStatus.Action.DECLINED)); - } - - ServerList.func_147414_b(serverData); - mc.displayGuiScreen(null); + 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))); } } From 6095aef3d1f462b409f1a3df6297f17b3462dce5 Mon Sep 17 00:00:00 2001 From: johni0702 Date: Sat, 28 Jan 2017 10:33:45 +0100 Subject: [PATCH 12/19] Fix loading and storing of default interpolator with settings --- .../replaymod/simplepathing/SPTimeline.java | 22 ++++++++++++++++++- .../simplepathing/gui/GuiEditKeyframe.java | 15 ++++++++----- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/replaymod/simplepathing/SPTimeline.java b/src/main/java/com/replaymod/simplepathing/SPTimeline.java index 26442b66..49558272 100644 --- a/src/main/java/com/replaymod/simplepathing/SPTimeline.java +++ b/src/main/java/com/replaymod/simplepathing/SPTimeline.java @@ -5,7 +5,6 @@ import com.google.common.collect.Iterables; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; import com.replaymod.pathing.properties.CameraProperties; -import com.replaymod.simplepathing.properties.ExplicitInterpolationProperty; import com.replaymod.pathing.properties.SpectatorProperty; import com.replaymod.pathing.properties.TimestampProperty; import com.replaymod.replaystudio.pathing.PathingRegistry; @@ -25,6 +24,7 @@ import com.replaymod.replaystudio.pathing.path.Timeline; import com.replaymod.replaystudio.pathing.property.Property; import com.replaymod.replaystudio.util.EntityPositionTracker; import com.replaymod.replaystudio.util.Location; +import com.replaymod.simplepathing.properties.ExplicitInterpolationProperty; import lombok.Getter; import net.minecraft.crash.CrashReport; import net.minecraft.crash.CrashReportCategory; @@ -108,6 +108,26 @@ public class SPTimeline implements PathingRegistry { } } + public Change setDefaultInterpolator(Interpolator interpolator) { + Preconditions.checkState(defaultInterpolatorType != null, "Default interpolator type not set."); + Validate.isInstanceOf(defaultInterpolatorType.getInterpolatorClass(), interpolator); + + registerPositionInterpolatorProperties(interpolator); + + Change change = CombinedChange.create( + positionPath.getSegments().stream() + // Ignore explicitly set segments + .filter(s -> !s.getStartKeyframe().getValue(ExplicitInterpolationProperty.PROPERTY).isPresent()) + // Ignore spectator segments + .filter(s -> !isSpectatorSegment(s)) + // Update interpolator for every remaining segment + // This will create a fragmented interpolator which is split by the updateInterpolators call + .map(s -> SetInterpolator.create(s, interpolator)).toArray(Change[]::new) + ); + change.apply(timeline); + return CombinedChange.createFromApplied(change, updateInterpolators()); + } + public boolean isTimeKeyframe(long time) { return timePath.getKeyframe(time) != null; } diff --git a/src/main/java/com/replaymod/simplepathing/gui/GuiEditKeyframe.java b/src/main/java/com/replaymod/simplepathing/gui/GuiEditKeyframe.java index 9cabba88..6f3a8577 100644 --- a/src/main/java/com/replaymod/simplepathing/gui/GuiEditKeyframe.java +++ b/src/main/java/com/replaymod/simplepathing/gui/GuiEditKeyframe.java @@ -1,7 +1,6 @@ package com.replaymod.simplepathing.gui; import com.replaymod.pathing.properties.CameraProperties; -import com.replaymod.simplepathing.properties.ExplicitInterpolationProperty; import com.replaymod.pathing.properties.TimestampProperty; import com.replaymod.replay.ReplayModReplay; import com.replaymod.replaystudio.pathing.change.Change; @@ -16,6 +15,7 @@ import com.replaymod.simplepathing.InterpolatorType; import com.replaymod.simplepathing.SPTimeline; import com.replaymod.simplepathing.SPTimeline.SPPath; import com.replaymod.simplepathing.Setting; +import com.replaymod.simplepathing.properties.ExplicitInterpolationProperty; import de.johni0702.minecraft.gui.container.AbstractGuiContainer; import de.johni0702.minecraft.gui.container.GuiPanel; import de.johni0702.minecraft.gui.element.GuiButton; @@ -251,10 +251,11 @@ public abstract class GuiEditKeyframe> extends Abst xField.getDouble(), yField.getDouble(), zField.getDouble(), yawField.getFloat(), pitchField.getFloat(), rollField.getFloat() ); + Interpolator interpolator = interpolationPanel.getSettingsPanel().createInterpolator(); if (interpolationPanel.getInterpolatorType() == InterpolatorType.DEFAULT) { - return CombinedChange.createFromApplied(positionChange, timeline.setInterpolatorToDefault(time)); + return CombinedChange.createFromApplied(positionChange, timeline.setInterpolatorToDefault(time), + timeline.setDefaultInterpolator(interpolator)); } else { - Interpolator interpolator = interpolationPanel.getSettingsPanel().createInterpolator(); return CombinedChange.createFromApplied(positionChange, timeline.setInterpolator(time, interpolator)); } } @@ -295,13 +296,17 @@ public abstract class GuiEditKeyframe> extends Abst Optional segment = path.getSegments().stream() .filter(s -> s.getStartKeyframe() == keyframe).findFirst(); if (segment.isPresent()) { + Interpolator interpolator = segment.get().getInterpolator(); + InterpolatorType type = InterpolatorType.fromClass(interpolator.getClass()); if (keyframe.getValue(ExplicitInterpolationProperty.PROPERTY).isPresent()) { - Interpolator interpolator = segment.get().getInterpolator(); - InterpolatorType type = InterpolatorType.fromClass(interpolator.getClass()); dropdown.setSelected(type); // trigger the callback once to display settings panel } else { setSettingsPanel(InterpolatorType.DEFAULT); } + if (getInterpolatorTypeNoDefault(type).getInterpolatorClass().isInstance(interpolator)) { + //noinspection unchecked + settingsPanel.loadSettings(interpolator); + } } else { // Disable dropdown if this is the last keyframe dropdown.setDisabled(); From 5cbfd2884e0fd6ee45d8748089aaef450c0d9d2f Mon Sep 17 00:00:00 2001 From: johni0702 Date: Sat, 28 Jan 2017 10:51:24 +0100 Subject: [PATCH 13/19] Generate interpolator settings from InterpolatorType enum --- .../simplepathing/InterpolatorType.java | 1 + .../com/replaymod/simplepathing/Setting.java | 24 ++++++++++--------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/replaymod/simplepathing/InterpolatorType.java b/src/main/java/com/replaymod/simplepathing/InterpolatorType.java index 1c02f201..bc35910e 100644 --- a/src/main/java/com/replaymod/simplepathing/InterpolatorType.java +++ b/src/main/java/com/replaymod/simplepathing/InterpolatorType.java @@ -14,6 +14,7 @@ public enum InterpolatorType { CUBIC("cubic", CubicSplineInterpolator.class, CubicSplineInterpolator::new), LINEAR("linear", LinearInterpolator.class, LinearInterpolator::new); + @Getter private String localizationKey; @Getter diff --git a/src/main/java/com/replaymod/simplepathing/Setting.java b/src/main/java/com/replaymod/simplepathing/Setting.java index 328714b5..79e432b2 100644 --- a/src/main/java/com/replaymod/simplepathing/Setting.java +++ b/src/main/java/com/replaymod/simplepathing/Setting.java @@ -2,22 +2,24 @@ package com.replaymod.simplepathing; import com.replaymod.core.SettingsRegistry; -import java.util.ArrayList; +import java.util.Arrays; +import java.util.stream.Collectors; public final class Setting extends SettingsRegistry.SettingKeys { public static final Setting PATH_PREVIEW = make("pathpreview", "pathpreview", true); - public static final SettingsRegistry.MultipleChoiceSettingKeys DEFAULT_INTERPOLATION = - new SettingsRegistry.MultipleChoiceSettingKeys<>( - "simplepathing", "interpolator", "replaymod.gui.settings.interpolator", - "replaymod.gui.editkeyframe.interpolator.cubic.name"); + public static final SettingsRegistry.MultipleChoiceSettingKeys DEFAULT_INTERPOLATION; static { - DEFAULT_INTERPOLATION.setChoices(new ArrayList() { - { - add("replaymod.gui.editkeyframe.interpolator.cubic.name"); - add("replaymod.gui.editkeyframe.interpolator.linear.name"); - } - }); + String format = "replaymod.gui.editkeyframe.interpolator.%s.name"; + DEFAULT_INTERPOLATION = new SettingsRegistry.MultipleChoiceSettingKeys<>( + "simplepathing", "interpolator", "replaymod.gui.settings.interpolator", + String.format(format, InterpolatorType.fromString("invalid returns default").getLocalizationKey()) + ); + DEFAULT_INTERPOLATION.setChoices( + Arrays.stream(InterpolatorType.values()).filter(i -> i != InterpolatorType.DEFAULT) + .map(i -> String.format(format, i.getLocalizationKey())) + .collect(Collectors.toList()) + ); } private static Setting make(String key, String displayName, T defaultValue) { From bdbffd5c33efba3c09687b54c23ffef3f7fb0f6d Mon Sep 17 00:00:00 2001 From: johni0702 Date: Sat, 28 Jan 2017 11:03:59 +0100 Subject: [PATCH 14/19] Add Catmull-Rom spline interpolator to simplepathing --- ReplayStudio | 2 +- .../simplepathing/InterpolatorType.java | 4 ++- .../replaymod/simplepathing/SPTimeline.java | 30 +++++++++++++++-- .../simplepathing/gui/GuiEditKeyframe.java | 31 ++++++++++++++++++ .../assets/replaymod/lang/en_US.lang | 4 +++ .../simplepathing/SPTimelineTest.java | 32 +++++++++++++++++++ 6 files changed, 99 insertions(+), 4 deletions(-) diff --git a/ReplayStudio b/ReplayStudio index a0b1f2b6..c6154766 160000 --- a/ReplayStudio +++ b/ReplayStudio @@ -1 +1 @@ -Subproject commit a0b1f2b6628c5296e4306ffd97122727b979b7a9 +Subproject commit c615476693961345d04320e69f6558527ab73a59 diff --git a/src/main/java/com/replaymod/simplepathing/InterpolatorType.java b/src/main/java/com/replaymod/simplepathing/InterpolatorType.java index bc35910e..78993e0d 100644 --- a/src/main/java/com/replaymod/simplepathing/InterpolatorType.java +++ b/src/main/java/com/replaymod/simplepathing/InterpolatorType.java @@ -1,5 +1,6 @@ package com.replaymod.simplepathing; +import com.replaymod.replaystudio.pathing.interpolation.CatmullRomSplineInterpolator; import com.replaymod.replaystudio.pathing.interpolation.CubicSplineInterpolator; import com.replaymod.replaystudio.pathing.interpolation.Interpolator; import com.replaymod.replaystudio.pathing.interpolation.LinearInterpolator; @@ -11,6 +12,7 @@ import java.util.function.Supplier; @AllArgsConstructor public enum InterpolatorType { DEFAULT("default", null, null), + CATMULL_ROM("catmullrom", CatmullRomSplineInterpolator.class, () -> new CatmullRomSplineInterpolator(0.5)), CUBIC("cubic", CubicSplineInterpolator.class, CubicSplineInterpolator::new), LINEAR("linear", LinearInterpolator.class, LinearInterpolator::new); @@ -34,7 +36,7 @@ public enum InterpolatorType { for (InterpolatorType t : values()) { if (t.getI18nName().equals(string)) return t; } - return CUBIC; //the default + return CATMULL_ROM; //the default } public static InterpolatorType fromClass(Class cls) { diff --git a/src/main/java/com/replaymod/simplepathing/SPTimeline.java b/src/main/java/com/replaymod/simplepathing/SPTimeline.java index 49558272..b3cd30ad 100644 --- a/src/main/java/com/replaymod/simplepathing/SPTimeline.java +++ b/src/main/java/com/replaymod/simplepathing/SPTimeline.java @@ -2,6 +2,8 @@ package com.replaymod.simplepathing; import com.google.common.base.Preconditions; import com.google.common.collect.Iterables; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; import com.replaymod.pathing.properties.CameraProperties; @@ -14,6 +16,7 @@ import com.replaymod.replaystudio.pathing.change.CombinedChange; import com.replaymod.replaystudio.pathing.change.SetInterpolator; import com.replaymod.replaystudio.pathing.change.UpdateKeyframeProperties; import com.replaymod.replaystudio.pathing.impl.TimelineImpl; +import com.replaymod.replaystudio.pathing.interpolation.CatmullRomSplineInterpolator; import com.replaymod.replaystudio.pathing.interpolation.CubicSplineInterpolator; import com.replaymod.replaystudio.pathing.interpolation.Interpolator; import com.replaymod.replaystudio.pathing.interpolation.LinearInterpolator; @@ -68,7 +71,7 @@ public class SPTimeline implements PathingRegistry { @Getter private EntityPositionTracker entityTracker; - private InterpolatorType defaultInterpolatorType = InterpolatorType.CUBIC; + private InterpolatorType defaultInterpolatorType = InterpolatorType.fromString("invalid string returns default"); public SPTimeline() { this(createInitialTimeline()); @@ -573,6 +576,11 @@ public class SPTimeline implements PathingRegistry { writer.value("linear"); } else if (interpolator instanceof CubicSplineInterpolator) { writer.value("cubic-spline"); + } else if (interpolator instanceof CatmullRomSplineInterpolator) { + writer.beginObject(); + writer.name("type").value("catmull-rom-spline"); + writer.name("alpha").value(((CatmullRomSplineInterpolator) interpolator).getAlpha()); + writer.endObject(); } else { throw new IOException("Unknown interpolator type: " + interpolator); } @@ -580,12 +588,30 @@ public class SPTimeline implements PathingRegistry { @Override public Interpolator deserializeInterpolator(JsonReader reader) throws IOException { - String type = reader.nextString(); + String type; + JsonObject args; + switch (reader.peek()) { + case STRING: + type = reader.nextString(); + args = null; + break; + case BEGIN_OBJECT: + args = new JsonParser().parse(reader).getAsJsonObject(); + type = args.get("type").getAsString(); + break; + default: + throw new IOException("Unexpected token: " + reader.peek()); + } switch (type) { case "linear": return new LinearInterpolator(); case "cubic-spline": return new CubicSplineInterpolator(); + case "catmull-rom-spline": + if (args == null || !args.has("alpha")) { + throw new IOException("Missing alpha value for catmull-rom-spline."); + } + return new CatmullRomSplineInterpolator(args.get("alpha").getAsDouble()); default: throw new IOException("Unknown interpolation type: " + type); diff --git a/src/main/java/com/replaymod/simplepathing/gui/GuiEditKeyframe.java b/src/main/java/com/replaymod/simplepathing/gui/GuiEditKeyframe.java index 6f3a8577..7fa41072 100644 --- a/src/main/java/com/replaymod/simplepathing/gui/GuiEditKeyframe.java +++ b/src/main/java/com/replaymod/simplepathing/gui/GuiEditKeyframe.java @@ -5,6 +5,7 @@ import com.replaymod.pathing.properties.TimestampProperty; import com.replaymod.replay.ReplayModReplay; import com.replaymod.replaystudio.pathing.change.Change; import com.replaymod.replaystudio.pathing.change.CombinedChange; +import com.replaymod.replaystudio.pathing.interpolation.CatmullRomSplineInterpolator; import com.replaymod.replaystudio.pathing.interpolation.CubicSplineInterpolator; import com.replaymod.replaystudio.pathing.interpolation.Interpolator; import com.replaymod.replaystudio.pathing.interpolation.LinearInterpolator; @@ -317,6 +318,9 @@ public abstract class GuiEditKeyframe> extends Abst removeElement(this.settingsPanel); switch (getInterpolatorTypeNoDefault(type)) { + case CATMULL_ROM: + settingsPanel = new CatmullRomSettingsPanel(); + break; case CUBIC: settingsPanel = new CubicSettingsPanel(); break; @@ -353,6 +357,33 @@ public abstract class GuiEditKeyframe> extends Abst public abstract I createInterpolator(); } + public class CatmullRomSettingsPanel extends SettingsPanel { + public final GuiLabel alphaLabel = new GuiLabel().setColor(Colors.BLACK) + .setI18nText("replaymod.gui.editkeyframe.interpolator.catmullrom.alpha"); + public final GuiNumberField alphaField = new GuiNumberField().setSize(100, 20).setPrecision(5) + .setMinValue(0).setValidateOnFocusChange(true); + + { + setLayout(new HorizontalLayout(HorizontalLayout.Alignment.CENTER)); + addElements(new HorizontalLayout.Data(0.5), alphaLabel, alphaField); + } + + @Override + public void loadSettings(CatmullRomSplineInterpolator interpolator) { + alphaField.setValue(interpolator.getAlpha()); + } + + @Override + public CatmullRomSplineInterpolator createInterpolator() { + return new CatmullRomSplineInterpolator(alphaField.getDouble()); + } + + @Override + protected CatmullRomSettingsPanel getThis() { + return this; + } + } + public class CubicSettingsPanel extends SettingsPanel { @Override diff --git a/src/main/resources/assets/replaymod/lang/en_US.lang b/src/main/resources/assets/replaymod/lang/en_US.lang index e68f9897..c901032b 100644 --- a/src/main/resources/assets/replaymod/lang/en_US.lang +++ b/src/main/resources/assets/replaymod/lang/en_US.lang @@ -292,6 +292,7 @@ replaymod.gui.settings.title=Replay Mod Settings replaymod.gui.settings.interpolation.linear=Linear replaymod.gui.settings.interpolation.cubic=Cubic +replaymod.gui.settings.interpolation.catmullrom=Catmull Rom replaymod.gui.settings.bitrate=Video Bitrate replaymod.gui.settings.framerate=Video Framerate @@ -376,6 +377,9 @@ replaymod.gui.editkeyframe.spec.method.shoulder.smoothness=Path Smoothness replaymod.gui.editkeyframe.interpolator=Interpolator replaymod.gui.editkeyframe.interpolator.default.name=Default Interpolator replaymod.gui.editkeyframe.interpolator.default.desc=Uses the default interpolator defined in the Replay Mod Settings. +replaymod.gui.editkeyframe.interpolator.catmullrom.name=Catmull Rom Spline Interpolator +replaymod.gui.editkeyframe.interpolator.catmullrom.desc=Calculates a catmull rom spline with customizable alpha value for precise tightness and smoothness of the camera path. +replaymod.gui.editkeyframe.interpolator.catmullrom.alpha=Alpha: replaymod.gui.editkeyframe.interpolator.cubic.name=Cubic Spline Interpolator replaymod.gui.editkeyframe.interpolator.cubic.desc=Calculates a cubic equation matrix for all points for a smooth camera path. replaymod.gui.editkeyframe.interpolator.linear.name=Linear Interpolator diff --git a/src/test/java/com/replaymod/simplepathing/SPTimelineTest.java b/src/test/java/com/replaymod/simplepathing/SPTimelineTest.java index b94c7a53..de249b0e 100644 --- a/src/test/java/com/replaymod/simplepathing/SPTimelineTest.java +++ b/src/test/java/com/replaymod/simplepathing/SPTimelineTest.java @@ -5,6 +5,7 @@ import com.replaymod.pathing.properties.CameraProperties; import com.replaymod.pathing.properties.SpectatorProperty; import com.replaymod.pathing.properties.TimestampProperty; import com.replaymod.replaystudio.pathing.change.Change; +import com.replaymod.replaystudio.pathing.interpolation.CatmullRomSplineInterpolator; import com.replaymod.replaystudio.pathing.interpolation.Interpolator; import com.replaymod.replaystudio.pathing.interpolation.LinearInterpolator; import com.replaymod.replaystudio.pathing.path.Keyframe; @@ -510,6 +511,37 @@ public class SPTimelineTest { assertValidInterpolators(SPPath.POSITION, 4); } + @Test(expected = IllegalArgumentException.class) + public void testSetDefaultInterpolatorDifferentType() { + addPosition(0, 0); + addPosition(1, 1); + impl.setDefaultInterpolator(new CatmullRomSplineInterpolator(42)); + } + + @Test + public void testSetDefaultInterpolatorSameType() { + impl.setDefaultInterpolatorType(InterpolatorType.CATMULL_ROM); + addPosition(0, 0); + addSpectator(1, 1); + addSpectator(2, 2); + addPosition(3, 3); + addPosition(4, 3); + impl.setDefaultInterpolator(new CatmullRomSplineInterpolator(42)); + assertValidInterpolators(SPPath.POSITION, 3); + assertIsCatmullRom(0, 42); + assertIsCatmullRom(2, 42); + assertIsCatmullRom(3, 42); + } + + private void assertIsCatmullRom(int index, double alpha) { + String str = prettyPrintInterpolators(impl, SPPath.POSITION); + Interpolator interpolator = Iterables.get(impl.getPositionPath().getSegments(), index).getInterpolator(); + assertTrue("Expected segment " + index + " to be catmull rom interpolator: " + str, + interpolator instanceof CatmullRomSplineInterpolator); + assertTrue("Expected interpolator of segment segment " + index + " to have alpha " + alpha + ": " + str, + ((CatmullRomSplineInterpolator) interpolator).getAlpha() == 42); + } + private void addPosition(int time, int expectedNumberOfInterpolators) { impl.addPositionKeyframe(time, 1, 2, 3, 4, 5, 6, -1); assertNotNull(impl.getKeyframe(SPPath.POSITION, time)); From f7460f9bf5bda8a0237781d77a098c12abe3dc17 Mon Sep 17 00:00:00 2001 From: johni0702 Date: Sat, 28 Jan 2017 11:44:04 +0100 Subject: [PATCH 15/19] Re-enable time path after path playback (fixes #55) --- src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java b/src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java index 8eb27501..e3eaf4bc 100644 --- a/src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java +++ b/src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java @@ -311,12 +311,14 @@ public class GuiPathing { @Override public void onSuccess(@Nullable Void result) { overlay.setCloseable(true); + timePath.setActive(true); } @Override public void onFailure(Throwable t) { t.printStackTrace(); overlay.setCloseable(true); + timePath.setActive(true); } }); } From d61d0ac6707874d5d1aa7bf55186072c8fb9e176 Mon Sep 17 00:00:00 2001 From: johni0702 Date: Sat, 28 Jan 2017 12:54:42 +0100 Subject: [PATCH 16/19] Fix hand of invisible spectated player being visible (fixes #59) --- .../extras/playeroverview/PlayerOverview.java | 12 ++++++++++++ .../com/replaymod/replay/camera/CameraEntity.java | 9 +++++++++ 2 files changed, 21 insertions(+) diff --git a/src/main/java/com/replaymod/extras/playeroverview/PlayerOverview.java b/src/main/java/com/replaymod/extras/playeroverview/PlayerOverview.java index 514f806b..774cce95 100644 --- a/src/main/java/com/replaymod/extras/playeroverview/PlayerOverview.java +++ b/src/main/java/com/replaymod/extras/playeroverview/PlayerOverview.java @@ -11,7 +11,10 @@ import com.replaymod.replay.events.ReplayCloseEvent; import com.replaymod.replay.events.ReplayOpenEvent; import net.minecraft.client.renderer.entity.RenderManager; import net.minecraft.client.renderer.entity.RenderPlayer; +import net.minecraft.entity.Entity; import net.minecraft.entity.player.EntityPlayer; +import net.minecraftforge.client.event.RenderHandEvent; +import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.fml.common.FMLCommonHandler; import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; import org.lwjgl.input.Keyboard; @@ -56,6 +59,7 @@ public class PlayerOverview implements Extra { }); FMLCommonHandler.instance().bus().register(this); + MinecraftForge.EVENT_BUS.register(this); RenderManager renderManager = mod.getMinecraft().getRenderManager(); @SuppressWarnings("unchecked") @@ -92,6 +96,14 @@ public class PlayerOverview implements Extra { hiddenPlayers.clear(); } + @SubscribeEvent + public void oRenderHand(RenderHandEvent event) { + Entity view = module.getCore().getMinecraft().getRenderViewEntity(); + if (view != null && isHidden(view.getUniqueID())) { + event.setCanceled(true); + } + } + public boolean isSavingEnabled() { return savingEnabled; } diff --git a/src/main/java/com/replaymod/replay/camera/CameraEntity.java b/src/main/java/com/replaymod/replay/camera/CameraEntity.java index d7351cc2..3d5e74a9 100755 --- a/src/main/java/com/replaymod/replay/camera/CameraEntity.java +++ b/src/main/java/com/replaymod/replay/camera/CameraEntity.java @@ -252,6 +252,15 @@ public class CameraEntity extends EntityPlayerSP { return false; } + @Override + public boolean isInvisible() { + Entity view = mc.getRenderViewEntity(); + if (view != this) { + return view.isInvisible(); + } + return super.isInvisible(); + } + @Override public ResourceLocation getLocationSkin() { Entity view = mc.getRenderViewEntity(); From db6b0ff5a6179d00ccb2395936a2401abac58ece Mon Sep 17 00:00:00 2001 From: johni0702 Date: Sat, 28 Jan 2017 13:07:51 +0100 Subject: [PATCH 17/19] Fix / by 0 when two spectator keyframes are closer than 50ms (fixes #60) --- .../replaymod/simplepathing/preview/PathPreviewRenderer.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/replaymod/simplepathing/preview/PathPreviewRenderer.java b/src/main/java/com/replaymod/simplepathing/preview/PathPreviewRenderer.java index 8c12a07b..f5fc7684 100644 --- a/src/main/java/com/replaymod/simplepathing/preview/PathPreviewRenderer.java +++ b/src/main/java/com/replaymod/simplepathing/preview/PathPreviewRenderer.java @@ -96,8 +96,8 @@ public class PathPreviewRenderer { if (spectator && entityTracker == null) { continue; // Cannot render spectator positions when entity tracker is not yet loaded } - // Spectator segments have 20 lines per second whereas normal segments have a fixed 100 - long steps = spectator ? diff / 50 : 100; + // Spectator segments have 20 lines per second (at least 10) whereas normal segments have a fixed 100 + long steps = spectator ? Math.max(diff / 50, 10) : 100; Triple prevPos = null; for (int i = 0; i <= steps; i++) { long time = start.getTime() + diff * i / steps; From 27fd25ae84b2aeedc0789d331fec423245719600 Mon Sep 17 00:00:00 2001 From: johni0702 Date: Sat, 28 Jan 2017 13:18:36 +0100 Subject: [PATCH 18/19] Fix crash when server responds with garbage to language query (fixes #51) --- src/main/java/com/replaymod/extras/LocalizationExtra.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/replaymod/extras/LocalizationExtra.java b/src/main/java/com/replaymod/extras/LocalizationExtra.java index 233d324f..50e21aee 100644 --- a/src/main/java/com/replaymod/extras/LocalizationExtra.java +++ b/src/main/java/com/replaymod/extras/LocalizationExtra.java @@ -82,7 +82,7 @@ public class LocalizationExtra implements Extra { availableLanguages.put(langcode, prop); return true; } catch (ApiException e) { - if (e.getError().getId() != 16) { // This language has not been translated + if (e.getError() == null || e.getError().getId() != 16) { // This language has not been translated e.printStackTrace(); } } catch(ConnectException ce) { From 1ffeb010d552137ec4351851c53b216d021ac05b Mon Sep 17 00:00:00 2001 From: johni0702 Date: Sun, 29 Jan 2017 11:14:54 +0100 Subject: [PATCH 19/19] Update jGui --- jGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jGui b/jGui index 94e4b4c3..a77d2f3f 160000 --- a/jGui +++ b/jGui @@ -1 +1 @@ -Subproject commit 94e4b4c3889528aaf62804148e3d771ecbee5611 +Subproject commit a77d2f3f50ba61ce68d2de66b798ec1e0a62723e