diff --git a/src/main/java/com/replaymod/pathing/PathingRegistry.java b/src/main/java/com/replaymod/pathing/PathingRegistry.java new file mode 100644 index 00000000..6ff70748 --- /dev/null +++ b/src/main/java/com/replaymod/pathing/PathingRegistry.java @@ -0,0 +1,17 @@ +package com.replaymod.pathing; + +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import com.replaymod.pathing.interpolation.Interpolator; +import com.replaymod.pathing.path.Timeline; + +import java.io.IOException; + +/** + * Contains mappings required for serialization. + */ +public interface PathingRegistry { + Timeline createTimeline(); + void serializeInterpolator(JsonWriter writer, Interpolator interpolator) throws IOException; + Interpolator deserializeInterpolator(JsonReader reader) throws IOException; +} diff --git a/src/main/java/com/replaymod/pathing/change/AddKeyframe.java b/src/main/java/com/replaymod/pathing/change/AddKeyframe.java new file mode 100644 index 00000000..4a14654d --- /dev/null +++ b/src/main/java/com/replaymod/pathing/change/AddKeyframe.java @@ -0,0 +1,62 @@ +package com.replaymod.pathing.change; + +import com.google.common.base.Preconditions; +import com.google.common.base.Predicates; +import com.google.common.collect.Iterables; +import com.replaymod.pathing.path.Keyframe; +import com.replaymod.pathing.path.Path; +import com.replaymod.pathing.path.Timeline; +import lombok.NonNull; + +/** + * Adds a new property. + */ +public final class AddKeyframe implements Change { + @NonNull + public static AddKeyframe create(Path path, long time) { + return new AddKeyframe(path.getTimeline().getPaths().indexOf(path), time); + } + + AddKeyframe(int path, long time) { + this.path = path; + this.time = time; + } + + /** + * Path index + */ + private final int path; + + /** + * Time at which the property should be injected. + */ + private final long time; + + /** + * Index of the newly created property. + */ + private int index; + + private boolean applied; + + @Override + public void apply(Timeline timeline) { + Preconditions.checkState(!applied, "Already applied!"); + + Path path = timeline.getPaths().get(this.path); + Keyframe keyframe = path.insert(time); + index = Iterables.indexOf(path.getKeyframes(), Predicates.equalTo(keyframe)); + + applied = true; + } + + @Override + public void undo(Timeline timeline) { + Preconditions.checkState(applied, "Not yet applied!"); + + Path path = timeline.getPaths().get(this.path); + path.remove(Iterables.get(path.getKeyframes(), index), true); + + applied = false; + } +} diff --git a/src/main/java/com/replaymod/pathing/change/AddPath.java b/src/main/java/com/replaymod/pathing/change/AddPath.java new file mode 100644 index 00000000..66593e74 --- /dev/null +++ b/src/main/java/com/replaymod/pathing/change/AddPath.java @@ -0,0 +1,35 @@ +package com.replaymod.pathing.change; + +import com.google.common.base.Preconditions; +import com.replaymod.pathing.path.Timeline; +import lombok.NonNull; + +/** + * Adds a new property. + */ +public final class AddPath implements Change { + @NonNull + public static AddPath create() { + return new AddPath(); + } + + private boolean applied; + + @Override + public void apply(Timeline timeline) { + Preconditions.checkState(!applied, "Already applied!"); + + timeline.createPath(); + + applied = true; + } + + @Override + public void undo(Timeline timeline) { + Preconditions.checkState(applied, "Not yet applied!"); + + timeline.getPaths().remove(timeline.getPaths().size() - 1); + + applied = false; + } +} diff --git a/src/main/java/com/replaymod/pathing/change/Change.java b/src/main/java/com/replaymod/pathing/change/Change.java new file mode 100644 index 00000000..d002636f --- /dev/null +++ b/src/main/java/com/replaymod/pathing/change/Change.java @@ -0,0 +1,29 @@ +package com.replaymod.pathing.change; + +import com.replaymod.pathing.path.Timeline; + +/** + * A change to any part of a timeline. + * If {@link #undo(Timeline)} is not called in the reverse order of {@link #apply(Timeline)}, the behavior is unspecified. + * Instances implementing this interfaces must not reference any outside objects required for applying + * as they might be serialized and deserialized at any time. + * After deserialization, only the {@link #apply(Timeline)} method is guaranteed to work. + */ +public interface Change { + + /** + * Apply this change. + * + * @param timeline The timeline + * @throws IllegalStateException If already applied. + */ + void apply(Timeline timeline); + + /** + * Undo this change. + * + * @param timeline The timeline + * @throws IllegalStateException If not yet applied. + */ + void undo(Timeline timeline); +} diff --git a/src/main/java/com/replaymod/pathing/change/CombinedChange.java b/src/main/java/com/replaymod/pathing/change/CombinedChange.java new file mode 100644 index 00000000..43bfe21f --- /dev/null +++ b/src/main/java/com/replaymod/pathing/change/CombinedChange.java @@ -0,0 +1,70 @@ +package com.replaymod.pathing.change; + +import com.google.common.base.Preconditions; +import com.replaymod.pathing.path.Timeline; +import lombok.NonNull; + +import java.util.Arrays; +import java.util.List; +import java.util.ListIterator; + +/** + * Represents multiple changes as one change. + */ +public class CombinedChange implements Change { + + /** + * Combines the specified changes into one change in the order given. + * All changes must not yet have been applied. + * + * @param changes List of changes + * @return A new CombinedChange instance + */ + @NonNull + public static CombinedChange create(Change... changes) { + return new CombinedChange(Arrays.asList(changes), false); + } + + /** + * Combines the specified changes into one change in the order given. + * All changes must have been applied. + * + * @param changes List of changes + * @return A new CombinedChange instance + */ + @NonNull + public static CombinedChange createFromApplied(Change... changes) { + return new CombinedChange(Arrays.asList(changes), true); + } + + CombinedChange(List changeList, boolean applied) { + this.changeList = changeList; + this.applied = applied; + } + + private final List changeList; + private boolean applied; + + @Override + public void apply(Timeline timeline) { + Preconditions.checkState(!applied, "Already applied!"); + + for (Change change : changeList) { + change.apply(timeline); + } + + applied = true; + } + + @Override + public void undo(Timeline timeline) { + Preconditions.checkState(applied, "Not yet applied!"); + + ListIterator iterator = changeList.listIterator(changeList.size()); + while (iterator.hasPrevious()) { + iterator.previous().undo(timeline); + } + + applied = false; + } +} diff --git a/src/main/java/com/replaymod/pathing/change/RemoveKeyframe.java b/src/main/java/com/replaymod/pathing/change/RemoveKeyframe.java new file mode 100644 index 00000000..9231ec83 --- /dev/null +++ b/src/main/java/com/replaymod/pathing/change/RemoveKeyframe.java @@ -0,0 +1,67 @@ +package com.replaymod.pathing.change; + +import com.google.common.base.Preconditions; +import com.google.common.base.Predicates; +import com.google.common.collect.Iterables; +import com.replaymod.pathing.interpolation.Interpolator; +import com.replaymod.pathing.path.Keyframe; +import com.replaymod.pathing.path.Path; +import com.replaymod.pathing.path.Timeline; +import lombok.NonNull; + +/** + * Removes a property. + */ +public final class RemoveKeyframe implements Change { + @NonNull + public static RemoveKeyframe create(@NonNull Path path, @NonNull Keyframe keyframe) { + return new RemoveKeyframe(path.getTimeline().getPaths().indexOf(path), + Iterables.indexOf(path.getKeyframes(), Predicates.equalTo(keyframe))); + } + + RemoveKeyframe(int path, int index) { + this.path = path; + this.index = index; + } + + /** + * Path index + */ + private final int path; + + /** + * Index of the property to be removed. + */ + private final int index; + + private volatile Keyframe removedKeyframe; + private volatile Interpolator removedInterpolator; + + private boolean applied; + + @Override + public void apply(Timeline timeline) { + Preconditions.checkState(!applied, "Already applied!"); + + Path path = timeline.getPaths().get(this.path); + if (index < path.getSegments().size()) { + removedInterpolator = Iterables.get(path.getSegments(), index).getInterpolator(); + } + path.remove(removedKeyframe = Iterables.get(path.getKeyframes(), index), true); + + applied = true; + } + + @Override + public void undo(Timeline timeline) { + Preconditions.checkState(applied, "Not yet applied!"); + + Path path = timeline.getPaths().get(this.path); + path.insert(removedKeyframe); + if (removedInterpolator != null) { + Iterables.get(path.getSegments(), index).setInterpolator(removedInterpolator); + } + + applied = false; + } +} diff --git a/src/main/java/com/replaymod/pathing/change/RemovePath.java b/src/main/java/com/replaymod/pathing/change/RemovePath.java new file mode 100644 index 00000000..765cf958 --- /dev/null +++ b/src/main/java/com/replaymod/pathing/change/RemovePath.java @@ -0,0 +1,50 @@ +package com.replaymod.pathing.change; + +import com.google.common.base.Preconditions; +import com.replaymod.pathing.path.Path; +import com.replaymod.pathing.path.Timeline; +import lombok.NonNull; + +/** + * Adds a new property. + */ +public final class RemovePath implements Change { + @NonNull + public static RemovePath create(Path path) { + return new RemovePath(path.getTimeline().getPaths().indexOf(path)); + } + + RemovePath(int path) { + this.path = path; + } + + /** + * Path index + */ + private final int path; + + /** + * The removed path + */ + private volatile Path oldPath; + + private boolean applied; + + @Override + public void apply(Timeline timeline) { + Preconditions.checkState(!applied, "Already applied!"); + + oldPath = timeline.getPaths().remove(path); + + applied = true; + } + + @Override + public void undo(Timeline timeline) { + Preconditions.checkState(applied, "Not yet applied!"); + + timeline.getPaths().add(path, oldPath); + + applied = false; + } +} diff --git a/src/main/java/com/replaymod/pathing/change/UpdateKeyframeProperties.java b/src/main/java/com/replaymod/pathing/change/UpdateKeyframeProperties.java new file mode 100644 index 00000000..2442aec4 --- /dev/null +++ b/src/main/java/com/replaymod/pathing/change/UpdateKeyframeProperties.java @@ -0,0 +1,131 @@ +package com.replaymod.pathing.change; + +import com.google.common.base.Optional; +import com.google.common.base.Preconditions; +import com.replaymod.pathing.path.Keyframe; +import com.replaymod.pathing.path.Path; +import com.replaymod.pathing.path.Timeline; +import com.replaymod.pathing.property.Property; +import com.replaymod.pathing.property.PropertyGroup; +import lombok.NonNull; + +import java.util.HashMap; +import java.util.Map; + +import static com.google.common.base.Predicates.equalTo; +import static com.google.common.collect.Iterables.get; +import static com.google.common.collect.Iterables.indexOf; + +/** + * Updates some properties of a property. + */ +public final class UpdateKeyframeProperties implements Change { + + private static String toId(Property property) { + assert property != null; + PropertyGroup group = property.getGroup(); + return (group != null ? group.getId() + ":" : "") + property.getId(); + } + + public static class Builder { + private final int path; + private final int keyframe; + private final Map> updates = new HashMap<>(); + + private Builder(int path, int keyframe) { + this.path = path; + this.keyframe = keyframe; + } + + /** + * Set the value for the property at this property. + * If the property is not present, adds it. + * + * @param property The property + * @param value Value of the property, may be {@code null} + * @param Type of the property + * @return {@code this} for chaining + */ + public Builder setValue(Property property, T value) { + updates.put(toId(property), Optional.of((Object) value)); + return this; + } + + /** + * Remove the specified property from this property. + * + * @param property The property to be removed + * @return {@code this} for chaining + */ + public Builder removeProperty(Property property) { + updates.put(toId(property), Optional.absent()); + return this; + } + + public UpdateKeyframeProperties done() { + return new UpdateKeyframeProperties(path, keyframe, updates); + } + } + + @NonNull + public static UpdateKeyframeProperties.Builder create(@NonNull Path path, @NonNull Keyframe keyframe) { + return new UpdateKeyframeProperties.Builder(path.getTimeline().getPaths().indexOf(path), + indexOf(path.getKeyframes(), equalTo(keyframe))); + } + + UpdateKeyframeProperties(int path, int index, Map> newValues) { + this.path = path; + this.index = index; + this.newValues = newValues; + } + + private final int path; + private final int index; + private final Map> newValues; + private final Map> oldValues = new HashMap<>(); + private boolean applied; + + @SuppressWarnings("unchecked") + @Override + public void apply(Timeline timeline) { + Preconditions.checkState(!applied, "Already applied!"); + + Path path = timeline.getPaths().get(this.path); + Keyframe keyframe = get(path.getKeyframes(), index); + for (Map.Entry> entry : newValues.entrySet()) { + Property property = timeline.getProperty(entry.getKey()); + if (property == null) throw new IllegalStateException("Property " + entry.getKey() + " unknown."); + Optional newValue = entry.getValue(); + oldValues.put(entry.getKey(), keyframe.getValue(property)); + if (newValue.isPresent()) { + keyframe.setValue(property, newValue.get()); + } else { + keyframe.removeProperty(property); + } + } + + applied = true; + } + + @SuppressWarnings("unchecked") + @Override + public void undo(Timeline timeline) { + Preconditions.checkState(applied, "Not yet applied!"); + + Path path = timeline.getPaths().get(this.path); + Keyframe keyframe = get(path.getKeyframes(), index); + for (Map.Entry> entry : oldValues.entrySet()) { + Property property = timeline.getProperty(entry.getKey()); + if (property == null) throw new IllegalStateException("Property " + entry.getKey() + " unknown."); + Optional oldValue = entry.getValue(); + newValues.put(entry.getKey(), keyframe.getValue(property)); + if (oldValue.isPresent()) { + keyframe.setValue(property, oldValue.get()); + } else { + keyframe.removeProperty(property); + } + } + + applied = false; + } +} diff --git a/src/main/java/com/replaymod/pathing/gui/GuiKeyframeRepository.java b/src/main/java/com/replaymod/pathing/gui/GuiKeyframeRepository.java new file mode 100644 index 00000000..6d80fa9d --- /dev/null +++ b/src/main/java/com/replaymod/pathing/gui/GuiKeyframeRepository.java @@ -0,0 +1,279 @@ +package com.replaymod.pathing.gui; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.SettableFuture; +import com.replaymod.core.ReplayMod; +import com.replaymod.pathing.PathingRegistry; +import com.replaymod.pathing.path.Timeline; +import com.replaymod.pathing.serialize.TimelineSerialization; +import com.replaymod.replay.ReplayModReplay; +import de.johni0702.minecraft.gui.GuiRenderer; +import de.johni0702.minecraft.gui.RenderInfo; +import de.johni0702.minecraft.gui.container.*; +import de.johni0702.minecraft.gui.element.GuiButton; +import de.johni0702.minecraft.gui.element.GuiLabel; +import de.johni0702.minecraft.gui.element.GuiTextField; +import de.johni0702.minecraft.gui.element.IGuiButton; +import de.johni0702.minecraft.gui.function.Closeable; +import de.johni0702.minecraft.gui.layout.CustomLayout; +import de.johni0702.minecraft.gui.layout.HorizontalLayout; +import de.johni0702.minecraft.gui.layout.VerticalLayout; +import de.johni0702.minecraft.gui.popup.GuiYesNoPopup; +import de.johni0702.minecraft.gui.utils.Colors; +import de.johni0702.minecraft.gui.utils.Consumer; +import de.johni0702.replaystudio.replay.ReplayFile; +import org.lwjgl.util.Dimension; +import org.lwjgl.util.ReadableDimension; + +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Gui for loading and saving {@link com.replaymod.pathing.path.Timeline Timelines}. + */ +public class GuiKeyframeRepository extends GuiScreen implements Closeable { + public final GuiPanel contentPanel = new GuiPanel(this).setBackgroundColor(Colors.DARK_TRANSPARENT); + public final GuiLabel title = new GuiLabel(contentPanel).setI18nText("replaymod.gui.keyframerepository.title"); + public final GuiVerticalList list = new GuiVerticalList(contentPanel).setDrawShadow(true).setDrawSlider(true); + public final GuiPanel buttonPanel = new GuiPanel(contentPanel).setLayout(new HorizontalLayout().setSpacing(5)); + public final GuiButton overwriteButton = new GuiButton(buttonPanel).onClick(new Runnable() { + @Override + public void run() { + timelines.put(selectedEntry.name, currentTimeline); + overwriteButton.setDisabled(); + save(); + } + }).setSize(75, 20).setI18nLabel("replaymod.gui.overwrite").setDisabled(); + public final GuiButton saveAsButton = new GuiButton(buttonPanel).onClick(new Runnable() { + @Override + public void run() { + final GuiTextField nameField = new GuiTextField().setSize(200, 20).setFocused(true); + final GuiYesNoPopup popup = GuiYesNoPopup.open(GuiKeyframeRepository.this, + new GuiLabel().setI18nText("replaymod.gui.saveas").setColor(Colors.BLACK), + nameField + ).setYesI18nLabel("replaymod.gui.save").setNoI18nLabel("replaymod.gui.cancel"); + popup.getYesButton().setDisabled(); + ((VerticalLayout) popup.getInfo().getLayout()).setSpacing(7); + nameField.onEnter(new Runnable() { + @Override + public void run() { + if (popup.getYesButton().isEnabled()) { + popup.getYesButton().onClick(); + } + } + }).onTextChanged(new Consumer() { + @Override + public void consume(String obj) { + popup.getYesButton().setEnabled(!nameField.getText().isEmpty() + && !timelines.containsKey(nameField.getText())); + } + }); + Futures.addCallback(popup.getFuture(), new FutureCallback() { + @Override + public void onSuccess(Boolean save) { + if (save) { + String name = nameField.getText(); + timelines.put(name, currentTimeline); + list.getListPanel().addElements(null, new Entry(name)); + save(); + } + } + + @Override + public void onFailure(Throwable t) { + t.printStackTrace(); + } + }); + } + }).setSize(75, 20).setI18nLabel("replaymod.gui.saveas"); + public final GuiButton loadButton = new GuiButton(buttonPanel).onClick(new Runnable() { + @Override + public void run() { + getMinecraft().displayGuiScreen(null); + future.set(timelines.get(selectedEntry.name)); + } + }).setSize(75, 20).setI18nLabel("replaymod.gui.load").setDisabled(); + public final GuiButton renameButton = new GuiButton(buttonPanel).onClick(new Runnable() { + @Override + public void run() { + final GuiTextField nameField = new GuiTextField().setSize(200, 20).setFocused(true).setText(selectedEntry.name); + final GuiYesNoPopup popup = GuiYesNoPopup.open(GuiKeyframeRepository.this, + new GuiLabel().setI18nText("replaymod.gui.rename").setColor(Colors.BLACK), + nameField + ).setYesI18nLabel("replaymod.gui.done").setNoI18nLabel("replaymod.gui.cancel"); + popup.getYesButton().setDisabled(); + ((VerticalLayout) popup.getInfo().getLayout()).setSpacing(7); + nameField.onEnter(new Runnable() { + @Override + public void run() { + if (popup.getYesButton().isEnabled()) { + popup.getYesButton().onClick(); + } + } + }).onTextChanged(new Consumer() { + @Override + public void consume(String obj) { + popup.getYesButton().setEnabled(!nameField.getText().isEmpty() + && !timelines.containsKey(nameField.getText())); + } + }); + Futures.addCallback(popup.getFuture(), new FutureCallback() { + @Override + public void onSuccess(Boolean save) { + if (save) { + String name = nameField.getText(); + timelines.put(name, timelines.remove(selectedEntry.name)); + selectedEntry.name = name; + selectedEntry.label.setText(name); + save(); + } + } + + @Override + public void onFailure(Throwable t) { + t.printStackTrace(); + } + }); + } + }).setSize(75, 20).setI18nLabel("replaymod.gui.rename").setDisabled(); + public final GuiButton removeButton = new GuiButton(buttonPanel).onClick(new Runnable() { + @Override + public void run() { + GuiYesNoPopup popup = GuiYesNoPopup.open(GuiKeyframeRepository.this, + new GuiLabel().setI18nText("replaymod.gui.keyframerepo.delete").setColor(Colors.BLACK) + ).setYesI18nLabel("replaymod.gui.delete").setNoI18nLabel("replaymod.gui.cancel"); + Futures.addCallback(popup.getFuture(), new FutureCallback() { + @Override + public void onSuccess(Boolean delete) { + if (delete) { + timelines.remove(selectedEntry.name); + list.getListPanel().removeElement(selectedEntry); + + selectedEntry = null; + overwriteButton.setDisabled(); + loadButton.setDisabled(); + renameButton.setDisabled(); + removeButton.setDisabled(); + save(); + } + } + + @Override + public void onFailure(Throwable t) { + t.printStackTrace(); + } + }); + } + }).setSize(75, 20).setI18nLabel("replaymod.gui.remove").setDisabled(); + + private final Map timelines = new LinkedHashMap<>(); + private final Timeline currentTimeline; + private final SettableFuture future = SettableFuture.create(); + private final TimelineSerialization serialization; + + private Entry selectedEntry; + + { + setDrawBackground(false); + setLayout(new CustomLayout() { + @Override + protected void layout(GuiScreen container, int width, int height) { + pos(contentPanel, width / 2 - width(contentPanel) / 2, height / 2 - height(contentPanel) / 2); + } + }); + contentPanel.setLayout(new CustomLayout() { + @Override + protected void layout(GuiPanel container, int width, int height) { + pos(title, width / 2 - width(title) / 2, 5); + size(list, width, height - 10 - height(buttonPanel) - 10 - y(title) - height(title) - 5); + pos(list, width / 2 - width(list) / 2, y(title) + height(title) + 5); + pos(buttonPanel, width / 2 - width(buttonPanel) / 2, y(list) + height(list) + 10); + } + + @Override + public ReadableDimension calcMinSize(GuiContainer container) { + ReadableDimension screenSize = getMinSize(); + return new Dimension(screenSize.getWidth() - 10, screenSize.getHeight() - 10); + } + }); + } + + public GuiKeyframeRepository(PathingRegistry registry, ReplayFile replayFile, Timeline currentTimeline) throws IOException { + this.currentTimeline = currentTimeline; + this.serialization = new TimelineSerialization(registry, replayFile); + + timelines.putAll(serialization.load()); + + for (Map.Entry entry : timelines.entrySet()) { + list.getListPanel().addElements(null, new Entry(entry.getKey())); + } + } + + @Override + public void display() { + super.display(); + ReplayModReplay.instance.getReplayHandler().getOverlay().setVisible(false); + } + + @Override + public void close() { + ReplayModReplay.instance.getReplayHandler().getOverlay().setVisible(true); + } + + public SettableFuture getFuture() { + return future; + } + + public void save() { + try { + serialization.save(timelines); + } catch (IOException e) { + e.printStackTrace(); + ReplayMod.instance.printWarningToChat("Error saving timelines: " + e.getMessage()); + } + } + + public class Entry extends AbstractGuiClickableContainer { + public final GuiLabel label = new GuiLabel(this); + private String name; + + public Entry(String name) { + this.name = name; + + setLayout(new CustomLayout() { + @Override + protected void layout(Entry container, int width, int height) { + pos(label, 5, height / 2 - height(label) / 2); + } + + @Override + public ReadableDimension calcMinSize(GuiContainer container) { + return new Dimension(buttonPanel.calcMinSize().getWidth(), 16); + } + }); + label.setText(name); + } + + @Override + protected void onClick() { + selectedEntry = this; + buttonPanel.forEach(IGuiButton.class).setEnabled(); + } + + @Override + public void draw(GuiRenderer renderer, ReadableDimension size, RenderInfo renderInfo) { + if (selectedEntry == this) { + renderer.drawRect(0, 0, size.getWidth(), size.getHeight(), Colors.BLACK); + renderer.drawRect(0, 0, 2, size.getHeight(), Colors.WHITE); + } + super.draw(renderer, size, renderInfo); + } + + @Override + protected Entry getThis() { + return this; + } + } +} diff --git a/src/main/java/com/replaymod/pathing/impl/KeyframeImpl.java b/src/main/java/com/replaymod/pathing/impl/KeyframeImpl.java new file mode 100644 index 00000000..5f5f596a --- /dev/null +++ b/src/main/java/com/replaymod/pathing/impl/KeyframeImpl.java @@ -0,0 +1,43 @@ +package com.replaymod.pathing.impl; + +import com.google.common.base.Optional; +import com.replaymod.pathing.path.Keyframe; +import com.replaymod.pathing.property.Property; +import lombok.RequiredArgsConstructor; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +@RequiredArgsConstructor +public class KeyframeImpl implements Keyframe { + private final long time; + private final Map properties = new HashMap<>(); + + @Override + public long getTime() { + return time; + } + + @SuppressWarnings("unchecked") + @Override + public Optional getValue(Property property) { + return properties.containsKey(property) ? Optional.of((T) properties.get(property)) : Optional.absent(); + } + + @Override + public void setValue(Property property, T value) { + properties.put(property, value); + } + + @Override + public void removeProperty(Property property) { + properties.remove(property); + } + + @Override + public Set getProperties() { + return Collections.unmodifiableSet(properties.keySet()); + } +} diff --git a/src/main/java/com/replaymod/pathing/impl/PathImpl.java b/src/main/java/com/replaymod/pathing/impl/PathImpl.java new file mode 100644 index 00000000..2547e291 --- /dev/null +++ b/src/main/java/com/replaymod/pathing/impl/PathImpl.java @@ -0,0 +1,175 @@ +package com.replaymod.pathing.impl; + +import com.google.common.base.Optional; +import com.replaymod.pathing.interpolation.InterpolationParameters; +import com.replaymod.pathing.interpolation.Interpolator; +import com.replaymod.pathing.path.Keyframe; +import com.replaymod.pathing.path.Path; +import com.replaymod.pathing.path.PathSegment; +import com.replaymod.pathing.path.Timeline; +import com.replaymod.pathing.property.Property; +import com.replaymod.pathing.property.PropertyPart; + +import java.util.*; + +public class PathImpl implements Path { + private final Timeline timeline; + private Map keyframes = new TreeMap<>(); + private List segments = new LinkedList<>(); + + public PathImpl(Timeline timeline) { + this.timeline = timeline; + } + + @Override + public Timeline getTimeline() { + return timeline; + } + + @Override + public Collection getKeyframes() { + return Collections.unmodifiableCollection(keyframes.values()); + } + + @Override + public Collection getSegments() { + return Collections.unmodifiableCollection(segments); + } + + @Override + public void update() { + update(false); + } + + @Override + public void updateAll() { + update(false); + } + + private void update(boolean force) { + Interpolator interpolator = null; + Map parameters = new HashMap<>(); + for (PathSegment segment : segments) { + if (segment.getInterpolator() != interpolator) { + interpolator = segment.getInterpolator(); + if (force || interpolator.isDirty()) { + parameters = interpolator.bake(parameters); + } + } + } + } + + @Override + public Optional getValue(Property property, long time) { + PathSegment segment = getSegment(time); + if (segment != null) { + Interpolator interpolator = segment.getInterpolator(); + if (interpolator != null) { + if (interpolator.getKeyframeProperties().contains(property)) { + return interpolator.getValue(property, time); + } + } + } + return Optional.absent(); + } + + @Override + public Keyframe insert(long time) { + Keyframe keyframe = new KeyframeImpl(time); + insert(keyframe); + return keyframe; + } + + @Override + public Keyframe getKeyframe(long time) { + return keyframes.get(time); + } + + @Override + public void insert(Keyframe keyframe) { + if (keyframes.containsKey(keyframe.getTime())) { + throw new IllegalStateException("A keyframe at " + keyframe.getTime() + " already exists."); + } + keyframes.put(keyframe.getTime(), keyframe); + + if (segments.isEmpty()) { + if (keyframes.size() >= 2) { + Iterator iter = keyframes.values().iterator(); + segments.add(new PathSegmentImpl(iter.next(), iter.next())); + } + return; + } + + ListIterator iter = segments.listIterator(); + PathSegment next = iter.next(); + if (keyframe.getTime() < next.getStartKeyframe().getTime()) { + iter.previous(); + iter.add(new PathSegmentImpl(keyframe, next.getStartKeyframe(), next.getInterpolator())); + } + + while (true) { + if (next.getStartKeyframe().getTime() <= keyframe.getTime() + && next.getEndKeyframe().getTime() >= keyframe.getTime()) { + iter.remove(); + next.setInterpolator(null); + iter.add(new PathSegmentImpl(next.getStartKeyframe(), keyframe, next.getInterpolator())); + iter.add(new PathSegmentImpl(keyframe, next.getEndKeyframe(), next.getInterpolator())); + return; + } + if (iter.hasNext()) { + next = iter.next(); + } else { + iter.add(new PathSegmentImpl(next.getEndKeyframe(), keyframe, next.getInterpolator())); + return; + } + } + } + + @Override + public void remove(Keyframe keyframe, boolean useFirstInterpolator) { + if (keyframes.get(keyframe.getTime()) != keyframe) { + throw new IllegalArgumentException("The keyframe " + keyframe + " is not part of this path."); + } + keyframes.remove(keyframe.getTime()); + + if (segments.size() < 2) { + for (PathSegment segment : segments) { + segment.setInterpolator(null); + } + segments.clear(); + return; + } + + ListIterator iter = segments.listIterator(); + while (iter.hasNext()) { + PathSegment next = iter.next(); + if (next.getEndKeyframe() == keyframe) { + iter.remove(); + if (iter.hasNext()) { + PathSegment next2 = iter.next(); + iter.remove(); + iter.add(new PathSegmentImpl(next.getStartKeyframe(), next2.getEndKeyframe(), + (useFirstInterpolator ? next : next2).getInterpolator())); + } else { + next.setInterpolator(null); + } + return; + } + if (next.getStartKeyframe() == keyframe) { + next.setInterpolator(null); + iter.remove(); + return; + } + } + throw new AssertionError("No segment for keyframe found!"); + } + + private PathSegment getSegment(long time) { + for (PathSegment segment : segments) { + if (segment.getStartKeyframe().getTime() <= time && segment.getEndKeyframe().getTime() >= time) { + return segment; + } + } + return null; + } +} diff --git a/src/main/java/com/replaymod/pathing/impl/PathSegmentImpl.java b/src/main/java/com/replaymod/pathing/impl/PathSegmentImpl.java new file mode 100644 index 00000000..9ba95354 --- /dev/null +++ b/src/main/java/com/replaymod/pathing/impl/PathSegmentImpl.java @@ -0,0 +1,32 @@ +package com.replaymod.pathing.impl; + +import com.replaymod.pathing.interpolation.Interpolator; +import com.replaymod.pathing.path.Keyframe; +import com.replaymod.pathing.path.PathSegment; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class PathSegmentImpl implements PathSegment { + private final Keyframe startKeyframe; + private final Keyframe endKeyframe; + private Interpolator interpolator; + + public PathSegmentImpl(Keyframe startKeyframe, Keyframe endKeyframe, Interpolator interpolator) { + this.startKeyframe = startKeyframe; + this.endKeyframe = endKeyframe; + setInterpolator(interpolator); + } + + @Override + public void setInterpolator(Interpolator interpolator) { + if (this.interpolator != null) { + this.interpolator.removeSegment(this); + } + this.interpolator = interpolator; + if (this.interpolator != null) { + this.interpolator.addSegment(this); + } + } +} diff --git a/src/main/java/com/replaymod/pathing/impl/TimelineImpl.java b/src/main/java/com/replaymod/pathing/impl/TimelineImpl.java new file mode 100644 index 00000000..9cf3744f --- /dev/null +++ b/src/main/java/com/replaymod/pathing/impl/TimelineImpl.java @@ -0,0 +1,101 @@ +package com.replaymod.pathing.impl; + +import com.google.common.base.Optional; +import com.replaymod.pathing.change.Change; +import com.replaymod.pathing.path.Path; +import com.replaymod.pathing.path.Timeline; +import com.replaymod.pathing.property.Property; +import com.replaymod.replay.ReplayHandler; + +import java.util.*; + +public class TimelineImpl implements Timeline { + private final List paths = new ArrayList<>(); + private Map properties = new HashMap<>(); + private Deque undoStack = new ArrayDeque<>(); + private Deque redoStack = new ArrayDeque<>(); + + @Override + public List getPaths() { + return paths; + } + + @Override + public Path createPath() { + Path path = new PathImpl(this); + paths.add(path); + return path; + } + + @Override + public Optional getValue(Property property, long time) { + for (Path path : paths) { + Optional value = path.getValue(property, time); + if (value.isPresent()) { + return value; + } + } + return Optional.absent(); + } + + @Override + public void applyToGame(long time, ReplayHandler replayHandler) { + for (Property property : properties.values()) { + applyToGame(time, replayHandler, property); + } + } + + private void applyToGame(long time, ReplayHandler replayHandler, Property property) { + Optional value = getValue(property, time); + if (value.isPresent()) { + property.applyToGame(value.get(), replayHandler); + } + } + + @Override + public void registerProperty(Property property) { + String id = (property.getGroup() == null ? "" : property.getGroup().getId() + ":") + property.getId(); + properties.put(id, property); + } + + @Override + public Property getProperty(String id) { + return properties.get(id); + } + + @Override + public void applyChange(Change change) { + change.apply(this); + pushChange(change); + } + + @Override + public void pushChange(Change change) { + undoStack.push(change); + redoStack.clear(); + } + + @Override + public void undoLastChange() { + Change change = undoStack.pop(); + change.undo(this); + redoStack.push(change); + } + + @Override + public void redoLastChange() { + Change change = redoStack.pop(); + change.apply(this); + undoStack.push(change); + } + + @Override + public Change peekUndoStack() { + return undoStack.peek(); + } + + @Override + public Change peekRedoStack() { + return undoStack.peek(); + } +} diff --git a/src/main/java/com/replaymod/pathing/interpolation/AbstractInterpolator.java b/src/main/java/com/replaymod/pathing/interpolation/AbstractInterpolator.java new file mode 100644 index 00000000..375e9487 --- /dev/null +++ b/src/main/java/com/replaymod/pathing/interpolation/AbstractInterpolator.java @@ -0,0 +1,74 @@ +package com.replaymod.pathing.interpolation; + +import com.replaymod.pathing.path.PathSegment; +import com.replaymod.pathing.property.Property; +import com.replaymod.pathing.property.PropertyPart; + +import java.util.*; + +public abstract class AbstractInterpolator implements Interpolator { + private List segments = new LinkedList<>(); + private boolean dirty; + private final Set properties = new HashSet<>(); + + @Override + public Collection getKeyframeProperties() { + return Collections.unmodifiableCollection(properties); + } + + public void registerProperty(Property property) { + properties.add(property); + } + + @Override + public void addSegment(PathSegment segment) { + segments.add(segment); + dirty = true; + } + + @Override + public void removeSegment(PathSegment segment) { + segments.remove(segment); + dirty = true; + } + + @Override + public List getSegments() { + return Collections.unmodifiableList(segments); + } + + @Override + public Map bake(Map parameters) { + if (segments.isEmpty()) throw new IllegalStateException("No segments have been added yet."); + Collections.sort(segments, new Comparator() { + @Override + public int compare(PathSegment s1, PathSegment s2) { + return Long.compare(s1.getStartKeyframe().getTime(), s2.getStartKeyframe().getTime()); + } + }); + + // Check for continuity + Iterator iter = segments.iterator(); + PathSegment last = iter.next(); + while (iter.hasNext()) { + if (last.getEndKeyframe() != (last = iter.next()).getStartKeyframe()) { + throw new IllegalStateException("Segments are not continuous."); + } + } + + return bakeInterpolation(parameters); + } + + /** + * Bake the interpolation of the current path segments with the specified parameters. + * Order of {@link #getSegments()} is guaranteed. + * @param parameters Map of parameters for some properties + * @return Map of parameters for the next interpolator + */ + protected abstract Map bakeInterpolation(Map parameters); + + @Override + public boolean isDirty() { + return dirty; + } +} diff --git a/src/main/java/com/replaymod/pathing/interpolation/InterpolationParameters.java b/src/main/java/com/replaymod/pathing/interpolation/InterpolationParameters.java new file mode 100644 index 00000000..744410d0 --- /dev/null +++ b/src/main/java/com/replaymod/pathing/interpolation/InterpolationParameters.java @@ -0,0 +1,28 @@ +package com.replaymod.pathing.interpolation; + +import lombok.Data; + +/** + * Parameters which can be used to make an interpolation more fitting to the previous one. + */ +@Data +public class InterpolationParameters { + /** + * The final value. + * Should be the same as for the final property. + * If it differs though, this value should be preferred. + */ + private final double value; + + /** + * Velocity at the end of the interpolation. + * Normally this is the first derivative of the interpolating function at the end of the interpolation. + */ + private final double velocity; + + /** + * Acceleration at the end of the interpolation. + * Normally this is the second derivative of the interpolating function at the end of the interpolation. + */ + private final double acceleration; +} diff --git a/src/main/java/com/replaymod/pathing/interpolation/Interpolator.java b/src/main/java/com/replaymod/pathing/interpolation/Interpolator.java new file mode 100644 index 00000000..83636909 --- /dev/null +++ b/src/main/java/com/replaymod/pathing/interpolation/Interpolator.java @@ -0,0 +1,89 @@ +package com.replaymod.pathing.interpolation; + +import com.google.common.base.Optional; +import com.replaymod.pathing.path.Path; +import com.replaymod.pathing.path.PathSegment; +import com.replaymod.pathing.property.Property; +import com.replaymod.pathing.property.PropertyPart; +import lombok.NonNull; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * Interpolates multiple keyframes. Also provides all Properties it can handle. + * Each Interpolator may provide certain outputs for the next Interpolator. + * Interpolators are evaluated first to last. + */ +public interface Interpolator { + /** + * Returns a collection of all properties applicable for this Interpolator. + * + * @return Collection of properties or empty collection if none + */ + @NonNull + Collection getKeyframeProperties(); + + /** + * Add the specified path segment to this interpolator. + *

+ * This method should be called by the PathSegment itself. + * + * @param segment The path segments + */ + void addSegment(PathSegment segment); + + /** + * Remove the specified path segment from this interpolator. + *

+ * This method should be called by the PathSegment itself. + * + * @param segment The path segments + */ + void removeSegment(PathSegment segment); + + /** + * Returns an immutable list of all path segments handled by this interpolator. + * Ordering is only guaranteed after {@link #bake(Map)} has been called and only until + * the list is modified. + * + * @return List of path segments or empty list if none + */ + @NonNull + List getSegments(); + + /** + * Bake the interpolation of the current path segments with the specified parameters. + * Note that when this method is called directly, the path might need to be updated via {@link Path#update()} + * + * @param parameters Map of parameters for some properties + * @return Map of parameters for the next interpolator + * @throws IllegalStateException If no segments have been set yet + * or the segments are not continuous + */ + @NonNull + Map bake(Map parameters); + + /** + * Returns whether the segments handled by this interpolator have changed since the last + * call of {@link #bake(Map)}. + * This only includes the segments themselves, not the properties of their keyframes, those + * have to be tracked manually. + * @return {@code true} if segments have changed, {@code false} otherwise + */ + boolean isDirty(); + + /** + * Return the value of the property at the specified point in time. + * + * @param property The property + * @param time Time in milliseconds since the start + * @param Type of the property + * @return Optional value of the property + * @throws IllegalStateException If {@link #bake(Map)} has not yet been called + * or {@link #addSegment(PathSegment)}/{@link #removeSegment(PathSegment)} + * has been changed since the last bake + */ + Optional getValue(Property property, long time); +} diff --git a/src/main/java/com/replaymod/pathing/interpolation/LinearInterpolator.java b/src/main/java/com/replaymod/pathing/interpolation/LinearInterpolator.java new file mode 100644 index 00000000..99659480 --- /dev/null +++ b/src/main/java/com/replaymod/pathing/interpolation/LinearInterpolator.java @@ -0,0 +1,89 @@ +package com.replaymod.pathing.interpolation; + +import com.google.common.base.Optional; +import com.replaymod.pathing.path.Keyframe; +import com.replaymod.pathing.path.PathSegment; +import com.replaymod.pathing.property.Property; +import com.replaymod.pathing.property.PropertyPart; + +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +public class LinearInterpolator extends AbstractInterpolator { + private Map> framesToProperty = new HashMap<>(); + + private void addToMap(Property property, Keyframe keyframe) { + Set set = framesToProperty.get(property); + if (set == null) { + framesToProperty.put(property, set = new LinkedHashSet<>()); + } + set.add(keyframe); + } + + @SuppressWarnings("unchecked") + @Override + protected Map bakeInterpolation(Map parameters) { + framesToProperty.clear(); + for (PathSegment segment : getSegments()) { + for (Property property : getKeyframeProperties()) { + if (segment.getStartKeyframe().getValue(property).isPresent()) { + addToMap(property, segment.getStartKeyframe()); + } + if (segment.getEndKeyframe().getValue(property).isPresent()) { + addToMap(property, segment.getEndKeyframe()); + } + } + } + + Keyframe lastKeyframe = getSegments().get(getSegments().size() - 1).getEndKeyframe(); + Map lastParameters = new HashMap<>(); + for (Property property : getKeyframeProperties()) { + Optional optionalValue = lastKeyframe.getValue(property); + if (optionalValue.isPresent()) { + Object value = optionalValue.get(); + for (PropertyPart part : property.getParts()) { + lastParameters.put(part, new InterpolationParameters(part.toDouble(value), 1, 0)); + } + } + } + return lastParameters; + } + + @Override + public Optional getValue(Property property, long time) { + Set kfSet = framesToProperty.get(property); + if (kfSet == null) { + return Optional.absent(); + } + Keyframe kfBefore = null, kfAfter = null; + for (Keyframe keyframe : kfSet) { + if (keyframe.getTime() == time) { + return keyframe.getValue(property); + } else if (keyframe.getTime() < time) { + kfBefore = keyframe; + } else if (keyframe.getTime() > time) { + kfAfter = keyframe; + break; + } + } + if (kfBefore == null || kfAfter == null) { + return Optional.absent(); + } + + T valueBefore = kfBefore.getValue(property).get(); + T valueAfter = kfAfter.getValue(property).get(); + double fraction = (time - kfBefore.getTime()) / (double) (kfAfter.getTime() - kfBefore.getTime()); + + T interpolated = valueBefore; + for (PropertyPart part : property.getParts()) { + if (part.isInterpolatable()) { + double before = part.toDouble(valueBefore); + double after = part.toDouble(valueAfter); + interpolated = part.fromDouble(interpolated, (after - before) * fraction + before); + } + } + return Optional.of(interpolated); + } +} diff --git a/src/main/java/com/replaymod/pathing/path/Keyframe.java b/src/main/java/com/replaymod/pathing/path/Keyframe.java new file mode 100644 index 00000000..d50e10ff --- /dev/null +++ b/src/main/java/com/replaymod/pathing/path/Keyframe.java @@ -0,0 +1,55 @@ +package com.replaymod.pathing.path; + +import com.google.common.base.Optional; +import com.replaymod.pathing.property.Property; +import lombok.NonNull; + +import java.util.Set; + +/** + * Represents a key frame in time that is used to compute frames in between multiple key frames. + * Keyframes have different properties depending on the Interpolator tying them together. + * One property tied by a different Interpolator to the previous one than to the next one inherits both properties. + */ +public interface Keyframe { + + /** + * Return the time at which this property is set. + * + * @return Time in milliseconds since the start + */ + long getTime(); + + /** + * Return the value of the property set at this property. + * + * @param property The property + * @param Type of the property + * @return Optional value of the property + */ + @NonNull + Optional getValue(Property property); + + /** + * Set the value for the property at this property. + * If the property is not present, adds it. + * + * @param property The property + * @param value Value of the property, may be {@code null} + * @param Type of the property + */ + void setValue(Property property, T value); + + /** + * Remove the specified property from this property. + * + * @param property The property to be removed + */ + void removeProperty(Property property); + + /** + * Returns all properties of this property + * @return Set of properties + */ + Set getProperties(); +} diff --git a/src/main/java/com/replaymod/pathing/path/Path.java b/src/main/java/com/replaymod/pathing/path/Path.java new file mode 100644 index 00000000..f892b4f8 --- /dev/null +++ b/src/main/java/com/replaymod/pathing/path/Path.java @@ -0,0 +1,102 @@ +package com.replaymod.pathing.path; + +import com.google.common.base.Optional; +import com.replaymod.pathing.interpolation.Interpolator; +import com.replaymod.pathing.property.Property; +import lombok.NonNull; + +import java.util.Collection; +import java.util.Map; + +/** + * Represents a path for some object consisting of keyframes, path segments and interpolators. + */ +public interface Path { + /** + * Returns the timeline this path belongs to. + * @return The timeline + */ + Timeline getTimeline(); + + /** + * Return an immutable collection of all keyframes in this path. + * + * @return Collection of keyframes or empty list if none + */ + @NonNull + Collection getKeyframes(); + + /** + * Return an immutable collection of all segments of this path. + * + * @return Collection of segments or empty list if none + */ + @NonNull + Collection getSegments(); + + /** + * Update all interpolators that need updating. + * This does not detect changes to keyframes, only interpolators. + * If a property has changed, call {@link Interpolator#bake(Map)} before calling this method + * or call {@link #updateAll()}. + * + * @throws IllegalStateException If any path segments do not have an interpolator set. + */ + void update(); + + /** + * Update all interpolations. + * This method is significantly slower than {@link #update()}. + * + * @throws IllegalStateException If any path segments do not have an interpolator set. + */ + void updateAll(); + + /** + * Return the value of the property at the specified point in time. + * + * @param property The property + * @param time Time in milliseconds since the start + * @param Type of the property + * @return Optional value of the property + * @throws IllegalStateException If {@link #update()} has not yet been called + * or interpolators have changed since the last call + */ + Optional getValue(Property property, long time); + + /** + * Insert a new property at the specified time. + * The two new path segments inherit the interpolator of the previous one. + * Does not update the interpolators, call {@link #update()} to do so. + * + * @param time Time in milliseconds + * @return The new property + */ + Keyframe insert(long time); + + /** + * Returns the property at the specified time. + * @param time Time in milliseconds + * @return The property or {@code null} if none exists at that timestamp + */ + Keyframe getKeyframe(long time); + + /** + * Insert the specified property. + * The two new path segments inherit the interpolator of the previous one. + * Does not update the interpolators, call {@link #update()} to do so. + * + * @param keyframe The property + */ + void insert(Keyframe keyframe); + + /** + * Removes the specified property. + * Does not update the interpolators, call {@link #update()} to do so. + * + * @param keyframe The property to remove + * @param useFirstInterpolator {@code true} if the new path segment inherits the interpolator of the first segment, + * {@code false} if it inherits the one of the second segment + */ + void remove(Keyframe keyframe, boolean useFirstInterpolator); +} diff --git a/src/main/java/com/replaymod/pathing/path/PathSegment.java b/src/main/java/com/replaymod/pathing/path/PathSegment.java new file mode 100644 index 00000000..b83359ea --- /dev/null +++ b/src/main/java/com/replaymod/pathing/path/PathSegment.java @@ -0,0 +1,42 @@ +package com.replaymod.pathing.path; + +import com.replaymod.pathing.interpolation.Interpolator; +import lombok.NonNull; + +/** + * Represents a segment of a Path consisting of one start property and one end property. + * Each segment is interpolated by one Interpolator. Multiple segments may have the same Interpolator and can only + * be interpolated together (they may or may not influence each other indirectly). + */ +public interface PathSegment { + /** + * Return the start property of this segment. + * + * @return The first property + */ + @NonNull + Keyframe getStartKeyframe(); + + /** + * Return the end property of this segment. + * + * @return The second property + */ + @NonNull + Keyframe getEndKeyframe(); + + /** + * Return the interpolator responsible for this path segment. + * + * @return The interpolator or {@code null} if not yet set + * @throws IllegalStateException If no interpolator has been set yet + */ + @NonNull + Interpolator getInterpolator(); + + /** + * Set the interpolator responsible for this path segment. + * @param interpolator The interpolator + */ + void setInterpolator(Interpolator interpolator); +} diff --git a/src/main/java/com/replaymod/pathing/path/Timeline.java b/src/main/java/com/replaymod/pathing/path/Timeline.java new file mode 100644 index 00000000..49440b79 --- /dev/null +++ b/src/main/java/com/replaymod/pathing/path/Timeline.java @@ -0,0 +1,104 @@ +package com.replaymod.pathing.path; + +import com.google.common.base.Optional; +import com.replaymod.pathing.change.Change; +import com.replaymod.pathing.property.Property; +import com.replaymod.replay.ReplayHandler; + +import java.util.List; + +/** + * A timeline is a collection of paths that are played together. + */ +public interface Timeline { + /** + * Returns the list of paths that compose this timeline. + * The returned list can be modified and changes are reflected on the timeline. + * The order of the paths is their priority in case of conflicting properties. + * The first path to support a property is the one whose value is used. + * @return List of paths + */ + List getPaths(); + + /** + * Creates a new path and adds it to this timeline. + * @return A new Path instance + */ + Path createPath(); + + /** + * Return the value of the property at the specified point in time. + * + * @param property The property + * @param time Time in milliseconds since the start + * @param Type of the property + * @return Optional value of the property + * @throws IllegalStateException If {@link Path#update()} has not yet been called + * or interpolators have changed since the last call + */ + Optional getValue(Property property, long time); + + /** + * Apply the values of all properties at the specified time to the game. + * + * @param time The time on this path + * @param replayHandler The ReplayHandler instance + */ + void applyToGame(long time, ReplayHandler replayHandler); + + /** + * Registers the specified property for use in keyframes in this path. + * @param property The property + */ + void registerProperty(Property property); + + /** + * Returns the property corresponding to the specified id. + * The id is either "groupId:propertyId" or "propertyId" if the property doesn't belong to any group. + * @param id Id of the property + * @return The property or {@code null} if not existent + */ + Property getProperty(String id); + + /** + * Apply the change and push it on the undo stack. + * Clears the redo stack. + * @param change The change + * @throws IllegalStateException if the change has already been applied + */ + void applyChange(Change change); + + /** + * Push the change on the undo stack. + * Clears the redo stack. + * @param change The change + * @throws IllegalStateException if the change has not yet been applied + */ + void pushChange(Change change); + + /** + * Undo the last change and push it on the redo stack. + * @throws java.util.NoSuchElementException if the stack is empty + */ + void undoLastChange(); + + /** + * Redo the last undone change and push it back on the undo stack. + * @throws java.util.NoSuchElementException if the stack is empty + */ + void redoLastChange(); + + /** + * Peek at the top element of the undo stack. + * The returned element must never be undone manually. + * @return Top element on the undo stack, or {@code null} if the stack is empty + */ + Change peekUndoStack(); + + /** + * Peek at the top element of the redo stack. + * The returned element must never be redone manually. + * @return Top element on the redo stack, or {@code null} if the stack is empty + */ + Change peekRedoStack(); +} diff --git a/src/main/java/com/replaymod/pathing/player/AbstractTimelinePlayer.java b/src/main/java/com/replaymod/pathing/player/AbstractTimelinePlayer.java new file mode 100644 index 00000000..c0a579d0 --- /dev/null +++ b/src/main/java/com/replaymod/pathing/player/AbstractTimelinePlayer.java @@ -0,0 +1,90 @@ +package com.replaymod.pathing.player; + +import com.google.common.base.Function; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.Ordering; +import com.google.common.primitives.Longs; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; +import com.replaymod.pathing.path.Keyframe; +import com.replaymod.pathing.path.Path; +import com.replaymod.pathing.path.Timeline; +import com.replaymod.replay.ReplayHandler; +import net.minecraftforge.fml.common.FMLCommonHandler; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +import net.minecraftforge.fml.common.gameevent.TickEvent; + +import javax.annotation.Nullable; +import java.util.Iterator; + +/** + * Plays a timeline. + */ +public abstract class AbstractTimelinePlayer { + private final ReplayHandler replayHandler; + private Timeline timeline; + private long lastTimestamp; + private ListenableFuture future; + private SettableFuture settableFuture; + + public AbstractTimelinePlayer(ReplayHandler replayHandler) { + this.replayHandler = replayHandler; + } + + public ListenableFuture start(Timeline timeline) { + this.timeline = timeline; + + Iterator iter = FluentIterable.from(timeline.getPaths()) + .transformAndConcat(new Function>() { + @Nullable + @Override + public Iterable apply(@Nullable Path input) { + assert input != null; + return input.getKeyframes(); + } + }).iterator(); + if (!iter.hasNext()) { + lastTimestamp = 0; + } else { + lastTimestamp = new Ordering() { + @Override + public int compare(@Nullable Keyframe left, @Nullable Keyframe right) { + assert left != null; + assert right != null; + return Longs.compare(left.getTime(), right.getTime()); + } + }.max(iter).getTime(); + } + + replayHandler.getReplaySender().setSyncModeAndWait(); + FMLCommonHandler.instance().bus().register(this); + return future = settableFuture = SettableFuture.create(); + } + + public ListenableFuture getFuture() { + return future; + } + + public boolean isActive() { + return future != null && !future.isDone(); + } + + @SubscribeEvent + public void onTick(TickEvent.RenderTickEvent event) { + if (future.isDone()) { + replayHandler.getReplaySender().setAsyncMode(true); + FMLCommonHandler.instance().bus().unregister(this); + return; + } + long time = getTimePassed(); + if (time > lastTimestamp) { + time = lastTimestamp; + } + timeline.applyToGame(time, replayHandler); + if (time == lastTimestamp) { + settableFuture.set(null); + } + } + + public abstract long getTimePassed(); +} diff --git a/src/main/java/com/replaymod/pathing/player/RealtimeTimelinePlayer.java b/src/main/java/com/replaymod/pathing/player/RealtimeTimelinePlayer.java new file mode 100644 index 00000000..834dd912 --- /dev/null +++ b/src/main/java/com/replaymod/pathing/player/RealtimeTimelinePlayer.java @@ -0,0 +1,52 @@ +package com.replaymod.pathing.player; + +import com.google.common.util.concurrent.ListenableFuture; +import com.replaymod.pathing.path.Timeline; +import com.replaymod.replay.ReplayHandler; +import net.minecraftforge.fml.common.gameevent.TickEvent; + +/** + * Timeline player using the system time. + */ +public class RealtimeTimelinePlayer extends AbstractTimelinePlayer { + /** + * Wether the next frame is the first frame. + * We only start measuring time from the second frame + * as the first might have to jump in time which might take time. + */ + private boolean firstFrame; + private boolean secondFrame; + + /** + * System time in milliseconds at the start. + */ + private long startTime; + + public RealtimeTimelinePlayer(ReplayHandler replayHandler) { + super(replayHandler); + } + + @Override + public ListenableFuture start(Timeline timeline) { + firstFrame = true; + return super.start(timeline); + } + + @Override + public void onTick(TickEvent.RenderTickEvent event) { + if (secondFrame) { + secondFrame = false; + startTime = System.currentTimeMillis(); + } + super.onTick(event); + if (firstFrame) { + firstFrame = false; + secondFrame = true; + } + } + + @Override + public long getTimePassed() { + return firstFrame ? 0 : System.currentTimeMillis() - startTime; + } +} diff --git a/src/main/java/com/replaymod/pathing/properties/CameraProperties.java b/src/main/java/com/replaymod/pathing/properties/CameraProperties.java new file mode 100644 index 00000000..05cee4bf --- /dev/null +++ b/src/main/java/com/replaymod/pathing/properties/CameraProperties.java @@ -0,0 +1,107 @@ +package com.replaymod.pathing.properties; + +import com.google.common.base.Optional; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import com.replaymod.pathing.change.Change; +import com.replaymod.pathing.property.AbstractProperty; +import com.replaymod.pathing.property.AbstractPropertyGroup; +import com.replaymod.pathing.property.PropertyPart; +import com.replaymod.replay.ReplayHandler; +import lombok.NonNull; +import org.apache.commons.lang3.tuple.Triple; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import java.util.concurrent.Callable; + +/** + * Properties for camera positioning. + */ +public class CameraProperties extends AbstractPropertyGroup { + public static final CameraProperties GROUP = new CameraProperties(); + public static final Position POSITION = new Position(); + public static final Rotation ROTATION = new Rotation(); + private CameraProperties() { + super("camera", "replaymod.gui.camera"); + } + + @Override + public Optional>> getSetter() { + return Optional.absent(); + } + + public static class Position extends AbstractProperty> { + public final PropertyPart> + X = new PropertyParts.ForDoubleTriple(this, true, PropertyParts.TripleElement.LEFT), + Y = new PropertyParts.ForDoubleTriple(this, true, PropertyParts.TripleElement.MIDDLE), + Z = new PropertyParts.ForDoubleTriple(this, true, PropertyParts.TripleElement.RIGHT); + + private Position() { + super("position", "replaymod.gui.position", GROUP, Triple.of(0d, 0d, 0d)); + } + + @Override + public Collection>> getParts() { + return Arrays.asList(X, Y, Z); + } + + @Override + public void applyToGame(Triple value, @NonNull ReplayHandler replayHandler) { + replayHandler.getCameraEntity().setCameraPosition(value.getLeft(), value.getMiddle(), value.getRight()); + } + + @Override + public void toJson(JsonWriter writer, Triple value) throws IOException { + writer.beginArray().value(value.getLeft()).value(value.getMiddle()).value(value.getRight()).endArray(); + } + + @Override + public Triple fromJson(JsonReader reader) throws IOException { + reader.beginArray(); + try { + return Triple.of(reader.nextDouble(), reader.nextDouble(), reader.nextDouble()); + } finally { + reader.endArray(); + } + } + } + + public static class Rotation extends AbstractProperty> { + public final PropertyPart> + YAW = new PropertyParts.ForFloatTriple(this, true, PropertyParts.TripleElement.LEFT), + PITCH = new PropertyParts.ForFloatTriple(this, true, PropertyParts.TripleElement.MIDDLE), + ROLL = new PropertyParts.ForFloatTriple(this, true, PropertyParts.TripleElement.RIGHT); + + private Rotation() { + super("rotation", "replaymod.gui.rotation", GROUP, Triple.of(0f, 0f, 0f)); + } + + @Override + public Collection>> getParts() { + return Arrays.asList(YAW, PITCH, ROLL); + } + + @Override + public void applyToGame(Triple value, @NonNull ReplayHandler replayHandler) { + replayHandler.getCameraEntity().setCameraRotation(value.getLeft(), value.getMiddle(), value.getRight()); + } + + @Override + public void toJson(JsonWriter writer, Triple value) throws IOException { + writer.beginArray().value(value.getLeft()).value(value.getMiddle()).value(value.getRight()).endArray(); + } + + @Override + public Triple fromJson(JsonReader reader) throws IOException { + reader.beginArray(); + try { + return Triple.of((float) reader.nextDouble(), (float) reader.nextDouble(), (float) reader.nextDouble()); + } finally { + reader.endArray(); + } + } + } +} diff --git a/src/main/java/com/replaymod/pathing/properties/PropertyParts.java b/src/main/java/com/replaymod/pathing/properties/PropertyParts.java new file mode 100644 index 00000000..28067ff5 --- /dev/null +++ b/src/main/java/com/replaymod/pathing/properties/PropertyParts.java @@ -0,0 +1,85 @@ +package com.replaymod.pathing.properties; + +import com.replaymod.pathing.property.AbstractPropertyPart; +import com.replaymod.pathing.property.Property; +import org.apache.commons.lang3.tuple.Triple; + +public class PropertyParts { + private PropertyParts(){} + + public static class ForInteger extends AbstractPropertyPart { + public ForInteger(Property property, boolean interpolatable) { + super(property, interpolatable); + } + + @Override + public double toDouble(Integer value) { + return value; + } + + @Override + public Integer fromDouble(Integer value, double d) { + return (int) Math.round(d); + } + } + + public static class ForDoubleTriple extends AbstractPropertyPart> { + private final TripleElement element; + public ForDoubleTriple(Property> property, boolean interpolatable, TripleElement element) { + super(property, interpolatable); + this.element = element; + } + + @Override + public double toDouble(Triple value) { + switch (element) { + case LEFT: return value.getLeft(); + case MIDDLE: return value.getMiddle(); + case RIGHT: return value.getRight(); + } + throw new AssertionError(element); + } + + @Override + public Triple fromDouble(Triple value, double d) { + switch (element) { + case LEFT: return Triple.of(d, value.getMiddle(), value.getRight()); + case MIDDLE: return Triple.of(value.getLeft(), d, value.getRight()); + case RIGHT: return Triple.of(value.getLeft(), value.getMiddle(), d); + } + throw new AssertionError(element); + } + } + + public static class ForFloatTriple extends AbstractPropertyPart> { + private final TripleElement element; + public ForFloatTriple(Property> property, boolean interpolatable, TripleElement element) { + super(property, interpolatable); + this.element = element; + } + + @Override + public double toDouble(Triple value) { + switch (element) { + case LEFT: return value.getLeft(); + case MIDDLE: return value.getMiddle(); + case RIGHT: return value.getRight(); + } + throw new AssertionError(element); + } + + @Override + public Triple fromDouble(Triple value, double d) { + switch (element) { + case LEFT: return Triple.of((float) d, value.getMiddle(), value.getRight()); + case MIDDLE: return Triple.of(value.getLeft(), (float) d, value.getRight()); + case RIGHT: return Triple.of(value.getLeft(), value.getMiddle(), (float) d); + } + throw new AssertionError(element); + } + } + + public enum TripleElement { + LEFT, MIDDLE, RIGHT; + } +} diff --git a/src/main/java/com/replaymod/pathing/properties/TimestampProperty.java b/src/main/java/com/replaymod/pathing/properties/TimestampProperty.java new file mode 100644 index 00000000..fa53ab5b --- /dev/null +++ b/src/main/java/com/replaymod/pathing/properties/TimestampProperty.java @@ -0,0 +1,49 @@ +package com.replaymod.pathing.properties; + +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import com.replaymod.pathing.property.AbstractProperty; +import com.replaymod.pathing.property.PropertyPart; +import com.replaymod.replay.ReplayHandler; +import com.replaymod.replay.ReplaySender; +import lombok.NonNull; + +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; + +/** + * Property for the time in the replay. + */ +public class TimestampProperty extends AbstractProperty { + public static final TimestampProperty PROPERTY = new TimestampProperty(); + public final PropertyPart TIME = new PropertyParts.ForInteger(this, true); + private TimestampProperty() { + super("timestamp", "replaymod.gui.editkeyframe.timestamp", null, 0); + } + + @Override + public Collection> getParts() { + return Collections.singletonList(TIME); + } + + @Override + public void applyToGame(Integer value, @NonNull ReplayHandler replayHandler) { + ReplaySender replaySender = replayHandler.getReplaySender(); + if (replaySender.isAsyncMode()) { + replaySender.jumpToTime(value); + } else { + replaySender.sendPacketsTill(value); + } + } + + @Override + public void toJson(JsonWriter writer, Integer value) throws IOException { + writer.value(value); + } + + @Override + public Integer fromJson(JsonReader reader) throws IOException { + return reader.nextInt(); + } +} diff --git a/src/main/java/com/replaymod/pathing/property/AbstractProperty.java b/src/main/java/com/replaymod/pathing/property/AbstractProperty.java new file mode 100644 index 00000000..61c886a0 --- /dev/null +++ b/src/main/java/com/replaymod/pathing/property/AbstractProperty.java @@ -0,0 +1,43 @@ +package com.replaymod.pathing.property; + +import net.minecraft.client.resources.I18n; + +/** + * Abstract base class for most properties. + */ +public abstract class AbstractProperty implements Property { + private final String id, localizationKey; + private final PropertyGroup propertyGroup; + private final T initialValue; + + public AbstractProperty(String id, String localizationKey, PropertyGroup propertyGroup, T initialValue) { + this.id = id; + this.localizationKey = localizationKey; + this.propertyGroup = propertyGroup; + this.initialValue = initialValue; + + if (propertyGroup != null) { + propertyGroup.getProperties().add(this); + } + } + + @Override + public String getLocalizedName() { + return I18n.format(localizationKey); + } + + @Override + public PropertyGroup getGroup() { + return propertyGroup; + } + + @Override + public String getId() { + return id; + } + + @Override + public T getNewValue() { + return initialValue; + } +} diff --git a/src/main/java/com/replaymod/pathing/property/AbstractPropertyGroup.java b/src/main/java/com/replaymod/pathing/property/AbstractPropertyGroup.java new file mode 100644 index 00000000..7625e99a --- /dev/null +++ b/src/main/java/com/replaymod/pathing/property/AbstractPropertyGroup.java @@ -0,0 +1,31 @@ +package com.replaymod.pathing.property; + +import lombok.RequiredArgsConstructor; +import net.minecraft.client.resources.I18n; + +import java.util.ArrayList; +import java.util.List; + +/** + * Abstract base class for most property groups. + */ +@RequiredArgsConstructor +public abstract class AbstractPropertyGroup implements PropertyGroup { + private final String id, localizationKey; + private final List properties = new ArrayList<>(); + + @Override + public String getLocalizedName() { + return I18n.format(localizationKey); + } + + @Override + public String getId() { + return id; + } + + @Override + public List getProperties() { + return properties; + } +} diff --git a/src/main/java/com/replaymod/pathing/property/AbstractPropertyPart.java b/src/main/java/com/replaymod/pathing/property/AbstractPropertyPart.java new file mode 100644 index 00000000..a5e7974c --- /dev/null +++ b/src/main/java/com/replaymod/pathing/property/AbstractPropertyPart.java @@ -0,0 +1,21 @@ +package com.replaymod.pathing.property; + +public abstract class AbstractPropertyPart implements PropertyPart { + private final Property property; + private final boolean interpolatable; + + public AbstractPropertyPart(Property property, boolean interpolatable) { + this.property = property; + this.interpolatable = interpolatable; + } + + @Override + public Property getProperty() { + return property; + } + + @Override + public boolean isInterpolatable() { + return interpolatable; + } +} diff --git a/src/main/java/com/replaymod/pathing/property/Property.java b/src/main/java/com/replaymod/pathing/property/Property.java new file mode 100644 index 00000000..c8f45413 --- /dev/null +++ b/src/main/java/com/replaymod/pathing/property/Property.java @@ -0,0 +1,78 @@ +package com.replaymod.pathing.property; + +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import com.replaymod.replay.ReplayHandler; +import lombok.NonNull; + +import java.io.IOException; +import java.util.Collection; + +/** + * Represents a property of a property. + * Such properties may or may not be interpolated between keyframes. + *
+ * If a property cannot be interpolated between keyframes, it is only active between two keyframes having the same + * value for that property. + * + * @param The type of the property, must be immutable + */ +public interface Property { + /** + * Returns the localized name of this property. + * + * @return Localized name. + */ + @NonNull + String getLocalizedName(); + + /** + * Returns the group this property belongs to. + * + * @return The group + */ + PropertyGroup getGroup(); + + /** + * Returns an ID unique for this property. + * There may be multiple instances for the same property ID + * if they all represent the same concept (all x-Coordinate). + * + * @return Unique ID + */ + @NonNull + String getId(); + + /** + * Returns a new value for this property. + * @return New value, may be {@code null} + */ + T getNewValue(); + + /** + * Returns (optionally) interpolatable parts of this property. + * @return Collection of parts + */ + Collection> getParts(); + + /** + * Appy the specified value of this property to the game. + * @param value The value of this property + * @param replayHandler The ReplayHandler instance + */ + void applyToGame(T value, ReplayHandler replayHandler); + + /** + * Writes the specified value of this property to JSON. + * @param writer The json writer + * @param value The value + */ + void toJson(JsonWriter writer, T value) throws IOException; + + /** + * Reads the value of this property from JSON. + * @param reader The json reader + * @return The value + */ + T fromJson(JsonReader reader) throws IOException; +} diff --git a/src/main/java/com/replaymod/pathing/property/PropertyGroup.java b/src/main/java/com/replaymod/pathing/property/PropertyGroup.java new file mode 100644 index 00000000..c0eff268 --- /dev/null +++ b/src/main/java/com/replaymod/pathing/property/PropertyGroup.java @@ -0,0 +1,52 @@ +package com.replaymod.pathing.property; + +import com.google.common.base.Optional; +import com.google.common.util.concurrent.ListenableFuture; +import com.replaymod.pathing.change.Change; +import lombok.NonNull; + +import java.util.List; +import java.util.concurrent.Callable; + +/** + * Represents a group of property properties. + * These groups are displayed and stored together. + * The same property id may exist in multiple groups.
+ * Groups may also define a way of setting their properties in one simple way (e.g. "set to current position"). + */ +public interface PropertyGroup { + /** + * Returns the localized name of this group. + * + * @return Localized name. + */ + @NonNull + String getLocalizedName(); + + /** + * Returns an ID unique for this group. + * + * @return Unique ID + */ + @NonNull + String getId(); + + /** + * Return a list of all properties in this group. + * + * @return List of properties or empty list if none. + */ + @NonNull + List getProperties(); + + /** + * Return a Callable which can be used to set all properties in this group at once. + * The Callable returns a future which is completed once all properties have been updated. + * It is up to the caller to apply those changes to the path. If those changes are not applied + * immediately, their content may become invalid and applying them results in undefined behavior. + * + * @return Optional callable + */ + @NonNull + Optional>> getSetter(); +} diff --git a/src/main/java/com/replaymod/pathing/property/PropertyPart.java b/src/main/java/com/replaymod/pathing/property/PropertyPart.java new file mode 100644 index 00000000..ce834d44 --- /dev/null +++ b/src/main/java/com/replaymod/pathing/property/PropertyPart.java @@ -0,0 +1,34 @@ +package com.replaymod.pathing.property; + +/** + * Represents an (optionally) interpolatable part of a property (such as the X coordinate of the position property). + * @param Type of the property + */ +public interface PropertyPart { + Property getProperty(); + + /** + * Returns whether this part should be interpolated between keyframes. + * Examples would be x/y/z coordinates or sun location. + * Counterexamples are spectated entity id or + * @return {@code true} if this part should be interpolated, {@code false} otherwise + */ + boolean isInterpolatable(); + + /** + * Convert this part of the value to a double for interpolation. + * @param value The value, may be {@code null} + * @return A double representing this value + * @throws UnsupportedOperationException if this part is not interpolatable + */ + double toDouble(T value); + + /** + * Convert the specified double to this part of the value and return it combined with the value for other parts. + * @param value Value of other parts + * @param d Value for this part + * @return Combined value + * @throws UnsupportedOperationException if this part is not interpolatable + */ + T fromDouble(T value, double d); +} diff --git a/src/main/java/eu/crushedpixel/replaymod/utils/LegacyKeyframeSetAdapter.java b/src/main/java/com/replaymod/pathing/serialize/LegacyKeyframeSetAdapter.java similarity index 54% rename from src/main/java/eu/crushedpixel/replaymod/utils/LegacyKeyframeSetAdapter.java rename to src/main/java/com/replaymod/pathing/serialize/LegacyKeyframeSetAdapter.java index 0b2956ca..92617cab 100644 --- a/src/main/java/eu/crushedpixel/replaymod/utils/LegacyKeyframeSetAdapter.java +++ b/src/main/java/com/replaymod/pathing/serialize/LegacyKeyframeSetAdapter.java @@ -1,101 +1,121 @@ -package eu.crushedpixel.replaymod.utils; +package com.replaymod.pathing.serialize; import com.google.gson.Gson; import com.google.gson.TypeAdapter; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; -import eu.crushedpixel.replaymod.assets.CustomImageObject; -import eu.crushedpixel.replaymod.holders.*; import java.io.IOException; import java.util.ArrayList; import java.util.List; +import static com.replaymod.pathing.serialize.LegacyTimelineConverter.*; + public class LegacyKeyframeSetAdapter extends TypeAdapter { public LegacyKeyframeSetAdapter() { super(); } + @SuppressWarnings("unchecked") @Override public KeyframeSet[] read(JsonReader in) throws IOException { - List sets = new ArrayList(); + List sets = new ArrayList<>(); in.beginArray(); while(in.hasNext()) { //iterate over all array entries KeyframeSet set = new KeyframeSet(); - List keyframes = new ArrayList(); + List positionKeyframes = new ArrayList<>(); + List timeKeyframes = new ArrayList<>(); in.beginObject(); while(in.hasNext()) { //iterate over all object entries String jsonTag = in.nextName(); if("name".equals(jsonTag)) { - set.setName(in.nextString()); + set.name = in.nextString(); //TODO: Adapt to new Spectator Keyframe system } else if("positionKeyframes".equals(jsonTag)) { in.beginArray(); while(in.hasNext()) { - Keyframe newKeyframe = new Keyframe(); + Keyframe newKeyframe = new Keyframe<>(); Integer spectatedEntityID = null; in.beginObject(); while(in.hasNext()) { String jsonKeyframeTag = in.nextName(); if("value".equals(jsonKeyframeTag) || "position".equals(jsonKeyframeTag)) { SpectatorData spectatorData = new Gson().fromJson(in, SpectatorData.class); - newKeyframe.setValue(spectatorData.normalize()); + if (spectatorData.spectatedEntityID != null) { + newKeyframe.value = spectatorData; + } else { + newKeyframe.value = new AdvancedPosition(); + newKeyframe.value.x = spectatorData.x; + newKeyframe.value.y = spectatorData.y; + newKeyframe.value.z = spectatorData.z; + newKeyframe.value.yaw = spectatorData.yaw; + newKeyframe.value.pitch = spectatorData.pitch; + newKeyframe.value.roll = spectatorData.roll; + } } else if("realTimestamp".equals(jsonKeyframeTag)) { - newKeyframe.setRealTimestamp(in.nextInt()); + newKeyframe.realTimestamp = in.nextInt(); } else if("spectatedEntityID".equals(jsonKeyframeTag)) { spectatedEntityID = in.nextInt(); } } if(spectatedEntityID != null) { - newKeyframe.setValue(newKeyframe.getValue().asSpectatorData(spectatedEntityID)); + AdvancedPosition pos = newKeyframe.value; + SpectatorData spectatorData = new SpectatorData(); + spectatorData.spectatedEntityID = spectatedEntityID; + newKeyframe.value = spectatorData; + newKeyframe.value.x = pos.x; + newKeyframe.value.y = pos.y; + newKeyframe.value.z = pos.z; + newKeyframe.value.yaw = pos.yaw; + newKeyframe.value.pitch = pos.pitch; + newKeyframe.value.roll = pos.roll; } in.endObject(); - keyframes.add(newKeyframe); + positionKeyframes.add(newKeyframe); } in.endArray(); } else if("timeKeyframes".equals(jsonTag)) { in.beginArray(); while(in.hasNext()) { - Keyframe newKeyframe = new Keyframe(); + Keyframe newKeyframe = new Keyframe<>(); in.beginObject(); while(in.hasNext()) { String jsonKeyframeTag = in.nextName(); if("timestamp".equals(jsonKeyframeTag)) { - TimestampValue timestampValue = new TimestampValue(in.nextInt()); - newKeyframe.setValue(timestampValue); + TimestampValue timestampValue = new TimestampValue(); + timestampValue.value = in.nextInt(); + newKeyframe.value = timestampValue; } else if("value".equals(jsonKeyframeTag)) { - TimestampValue timestampValue = new Gson().fromJson(in, TimestampValue.class); - newKeyframe.setValue(timestampValue); + newKeyframe.value = new Gson().fromJson(in, TimestampValue.class); } else if("realTimestamp".equals(jsonKeyframeTag)) { - newKeyframe.setRealTimestamp(in.nextInt()); + newKeyframe.realTimestamp = in.nextInt(); } } in.endObject(); - keyframes.add(newKeyframe); + timeKeyframes.add(newKeyframe); } in.endArray(); } else if("customObjects".equals(jsonTag)) { - CustomImageObject[] customObjects = new Gson().fromJson(in, CustomImageObject[].class); - - set.setCustomObjects(customObjects); + set.customObjects = new Gson().fromJson(in, CustomImageObject[].class); } } in.endObject(); - set.setKeyframes(keyframes.toArray(new Keyframe[keyframes.size()])); + set.positionKeyframes = positionKeyframes.toArray(new Keyframe[positionKeyframes.size()]); + set.timeKeyframes = timeKeyframes.toArray(new Keyframe[timeKeyframes.size()]); sets.add(set); } in.endArray(); diff --git a/src/main/java/com/replaymod/pathing/serialize/LegacyTimelineConverter.java b/src/main/java/com/replaymod/pathing/serialize/LegacyTimelineConverter.java new file mode 100644 index 00000000..e0f3f743 --- /dev/null +++ b/src/main/java/com/replaymod/pathing/serialize/LegacyTimelineConverter.java @@ -0,0 +1,138 @@ +package com.replaymod.pathing.serialize; + +import com.google.common.base.Optional; +import com.google.gson.GsonBuilder; +import com.replaymod.pathing.PathingRegistry; +import com.replaymod.pathing.path.Path; +import com.replaymod.pathing.path.Timeline; +import com.replaymod.pathing.properties.CameraProperties; +import com.replaymod.pathing.properties.TimestampProperty; +import de.johni0702.replaystudio.replay.ReplayFile; +import org.apache.commons.lang3.tuple.Triple; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.*; + +public class LegacyTimelineConverter { + public static Map convert(PathingRegistry registry, ReplayFile replayFile) throws IOException { + KeyframeSet[] keyframeSets = readAndParse(replayFile); + if (keyframeSets == null) { + return Collections.emptyMap(); + } + + Map timelines = new LinkedHashMap<>(); + for (KeyframeSet keyframeSet : keyframeSets) { + timelines.put(keyframeSet.name, convert(registry, keyframeSet)); + } + return timelines; + } + + private static Optional read(ReplayFile replayFile) throws IOException { + Optional in = replayFile.get("paths.json"); + if (!in.isPresent()) { + in = replayFile.get("paths"); + } + return in; + } + + private static KeyframeSet[] parse(InputStream in) { + return new GsonBuilder() + .registerTypeAdapter(KeyframeSet[].class, new LegacyKeyframeSetAdapter()) + .create().fromJson(new InputStreamReader(in), KeyframeSet[].class); + } + + private static KeyframeSet[] readAndParse(ReplayFile replayFile) throws IOException { + Optional optIn = read(replayFile); + if (!optIn.isPresent()) { + return null; + } + KeyframeSet[] keyframeSets; + try (InputStream in = optIn.get()) { + keyframeSets = parse(in); + } + return keyframeSets; + } + + private static Timeline convert(PathingRegistry registry, KeyframeSet keyframeSet) { + Path path = registry.createTimeline().createPath(); + for (Keyframe positionKeyframe : keyframeSet.positionKeyframes) { + AdvancedPosition value = positionKeyframe.value; + com.replaymod.pathing.path.Keyframe keyframe = getKeyframe(path, positionKeyframe.realTimestamp); + keyframe.setValue(CameraProperties.POSITION, Triple.of(value.x, value.y, value.z)); + keyframe.setValue(CameraProperties.ROTATION, Triple.of(value.yaw, value.pitch, value.roll)); + if (value instanceof SpectatorData) { + // TODO Spectator keyframes + } + } + for (Keyframe timeKeyframe : keyframeSet.timeKeyframes) { + TimestampValue value = timeKeyframe.value; + com.replaymod.pathing.path.Keyframe keyframe = getKeyframe(path, timeKeyframe.realTimestamp); + keyframe.setValue(TimestampProperty.PROPERTY, (int) value.value); + } + return path.getTimeline(); + } + + private static com.replaymod.pathing.path.Keyframe getKeyframe(Path path, long time) { + com.replaymod.pathing.path.Keyframe keyframe = path.getKeyframe(time); + if (keyframe == null) { + keyframe = path.insert(time); + } + return keyframe; + } + + static class KeyframeSet { + String name; + Keyframe[] positionKeyframes; + Keyframe[] timeKeyframes; + CustomImageObject[] customObjects; + } + static class Keyframe { + int realTimestamp; + T value; + } + static class Position { + double x, y, z; + } + static class AdvancedPosition extends Position { + float pitch, yaw, roll; + } + static class SpectatorData extends AdvancedPosition { + Integer spectatedEntityID; + SpectatingMethod spectatingMethod; + SpectatorDataThirdPersonInfo thirdPersonInfo; + enum SpectatingMethod { + FIRST_PERSON, SHOULDER_CAM; + } + } + static class SpectatorDataThirdPersonInfo { + double shoulderCamDistance; + double shoulderCamPitchOffset; + double shoulderCamYawOffset; + double shoulderCamSmoothness; + } + static class TimestampValue { + double value; + } + static class CustomImageObject { + String name; + UUID linkedAsset; + float width, height; + float textureWidth, textureHeight; + Transformations transformations = new Transformations(); + } + static class Transformations { + Position defaultAnchor, defaultPosition, defaultOrientation, defaultScale; + NumberValue defaultOpacity; + + List anchorKeyframes; + List positionKeyframes; + List orientationKeyframes; + List scaleKeyframes; + List opacityKeyframes; + } + static class NumberValue { + double value; + } +} diff --git a/src/main/java/com/replaymod/pathing/serialize/TimelineSerialization.java b/src/main/java/com/replaymod/pathing/serialize/TimelineSerialization.java new file mode 100644 index 00000000..c4239236 --- /dev/null +++ b/src/main/java/com/replaymod/pathing/serialize/TimelineSerialization.java @@ -0,0 +1,193 @@ +package com.replaymod.pathing.serialize; + +import com.google.common.base.Charsets; +import com.google.common.base.Optional; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; +import com.replaymod.pathing.PathingRegistry; +import com.replaymod.pathing.interpolation.Interpolator; +import com.replaymod.pathing.path.Keyframe; +import com.replaymod.pathing.path.Path; +import com.replaymod.pathing.path.PathSegment; +import com.replaymod.pathing.path.Timeline; +import com.replaymod.pathing.property.Property; +import de.johni0702.replaystudio.replay.ReplayFile; +import org.apache.commons.io.IOUtils; + +import java.io.*; +import java.util.*; + +public class TimelineSerialization { + private static final String FILE_ENTRY = "timelines.json"; + + private final PathingRegistry registry; + private final ReplayFile replayFile; + + public TimelineSerialization(PathingRegistry registry, ReplayFile replayFile) { + this.registry = registry; + this.replayFile = replayFile; + } + + public void save(Map timelines) throws IOException { + String serialized = serialize(timelines); + try (OutputStream out = replayFile.write(FILE_ENTRY)) { + out.write(serialized.getBytes(Charsets.UTF_8)); + } + } + + public Map load() throws IOException { + Map timelines = LegacyTimelineConverter.convert(registry, replayFile); + + Optional optionalIn = replayFile.get(FILE_ENTRY); + if (optionalIn.isPresent()) { + String serialized; + try (InputStream in = optionalIn.get()) { + serialized = IOUtils.toString(in, Charsets.UTF_8); + } + Map deserialized = deserialize(serialized); + timelines.putAll(deserialized); + } + return timelines; + } + + public String serialize(Map timelines) throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter writer = new JsonWriter(stringWriter); + writer.beginObject(); + for (Map.Entry entry : timelines.entrySet()) { + Timeline timeline = entry.getValue(); + writer.name(entry.getKey()).beginArray(); + for (Path path : timeline.getPaths()) { + writer.beginObject(); + writer.name("keyframes").beginArray(); + for (Keyframe keyframe : path.getKeyframes()) { + writer.beginObject(); + writer.name("time").value(keyframe.getTime()); + writer.name("properties").beginObject(); + for (Property property : keyframe.getProperties()) { + writer.name((property.getGroup() == null ? "" : property.getGroup().getId()) + property.getId()); + writeProperty(writer, keyframe, property); + } + writer.endObject(); + writer.endObject(); + } + writer.endArray(); + Map interpolators = new LinkedHashMap<>(); + writer.name("segments").beginArray(); + for (PathSegment segment : path.getSegments()) { + Interpolator interpolator = segment.getInterpolator(); + if (interpolator == null) { + writer.nullValue(); + } else { + Integer index = interpolators.get(interpolator); + if (index == null) { + interpolators.put(interpolator, index = interpolators.size()); + } + writer.value(index); + } + } + writer.endArray(); + writer.name("interpolators").beginArray(); + for (Interpolator interpolator : interpolators.keySet()) { + registry.serializeInterpolator(writer, interpolator); + } + writer.endArray(); + writer.endObject(); + } + writer.endArray(); + } + writer.endObject(); + writer.flush(); + return stringWriter.toString(); + } + + private static void writeProperty(JsonWriter writer, Keyframe keyframe, Property property) throws IOException { + property.toJson(writer, keyframe.getValue(property).get()); + } + + public Map deserialize(String serialized) throws IOException { + JsonReader reader = new JsonReader(new StringReader(serialized)); + Map timelines = new LinkedHashMap<>(); + reader.beginObject(); + while (reader.hasNext()) { + Timeline timeline = registry.createTimeline(); + timelines.put(reader.nextName(), timeline); + reader.beginArray(); + while (reader.hasNext()) { + Path path = timeline.createPath(); + reader.beginObject(); + List segments = new ArrayList<>(); + List interpolators = new ArrayList<>(); + while (reader.hasNext()) { + switch (reader.nextName()) { + case "keyframes": + reader.beginArray(); + while (reader.hasNext()) { + long time = 0; + Map properties = new HashMap<>(); + reader.beginObject(); + while (reader.hasNext()) { + switch (reader.nextName()) { + case "time": + time = reader.nextLong(); + break; + case "properties": + reader.beginObject(); + while (reader.hasNext()) { + String id = reader.nextName(); + Property property = timeline.getProperty(id); + if (property == null) { + throw new IOException("Unknown property: " + id); + } + Object value = property.fromJson(reader); + properties.put(property, value); + } + reader.endObject(); + break; + } + } + reader.endObject(); + Keyframe keyframe = path.insert(time); + for (Map.Entry entry : properties.entrySet()) { + keyframe.setValue(entry.getKey(), entry.getValue()); + } + } + reader.endArray(); + break; + case "segments": + reader.beginArray(); + while (reader.hasNext()) { + if (reader.peek() == JsonToken.NULL) { + reader.nextNull(); + segments.add(null); + } else { + segments.add(reader.nextInt()); + } + } + reader.endArray(); + break; + case "interpolators": + reader.beginArray(); + while (reader.hasNext()) { + interpolators.add(registry.deserializeInterpolator(reader)); + } + reader.endArray(); + break; + } + } + Iterator iter = segments.iterator(); + for (PathSegment segment : path.getSegments()) { + Integer next = iter.next(); + if (next != null) { + segment.setInterpolator(interpolators.get(next)); + } + } + reader.endObject(); + } + reader.endArray(); + } + reader.endObject(); + return timelines; + } +} diff --git a/src/main/java/com/replaymod/replay/ReplaySender.java b/src/main/java/com/replaymod/replay/ReplaySender.java index 7dda40a4..71de0604 100755 --- a/src/main/java/com/replaymod/replay/ReplaySender.java +++ b/src/main/java/com/replaymod/replay/ReplaySender.java @@ -3,10 +3,10 @@ package com.replaymod.replay; import com.google.common.base.Preconditions; import com.google.common.io.Files; import com.google.common.util.concurrent.ListenableFutureTask; +import com.replaymod.core.utils.Restrictions; import com.replaymod.replay.camera.CameraEntity; import de.johni0702.replaystudio.replay.ReplayFile; import eu.crushedpixel.replaymod.holders.PacketData; -import com.replaymod.core.utils.Restrictions; import eu.crushedpixel.replaymod.settings.ReplaySettings; import eu.crushedpixel.replaymod.utils.ReplayFileIO; import io.netty.channel.ChannelHandler.Sharable; @@ -191,6 +191,10 @@ public class ReplaySender extends ChannelInboundHandlerAdapter { } } + public boolean isAsyncMode() { + return asyncMode; + } + /** * Set whether this replay sender to operate in sync mode. * When in sync mode, it will send packets when {@link #sendPacketsTill(int)} is called. diff --git a/src/main/java/com/replaymod/simplepathing/ReplayModSimplePathing.java b/src/main/java/com/replaymod/simplepathing/ReplayModSimplePathing.java new file mode 100644 index 00000000..04af8097 --- /dev/null +++ b/src/main/java/com/replaymod/simplepathing/ReplayModSimplePathing.java @@ -0,0 +1,103 @@ +package com.replaymod.simplepathing; + +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import com.replaymod.core.ReplayMod; +import com.replaymod.pathing.PathingRegistry; +import com.replaymod.pathing.impl.TimelineImpl; +import com.replaymod.pathing.interpolation.AbstractInterpolator; +import com.replaymod.pathing.interpolation.Interpolator; +import com.replaymod.pathing.interpolation.LinearInterpolator; +import com.replaymod.pathing.path.Keyframe; +import com.replaymod.pathing.path.Timeline; +import com.replaymod.pathing.properties.CameraProperties; +import com.replaymod.pathing.properties.TimestampProperty; +import com.replaymod.replay.events.ReplayOpenEvent; +import com.replaymod.simplepathing.gui.GuiPathing; +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.IOException; + +@Mod(modid = ReplayModSimplePathing.MOD_ID, useMetadata = true) +public class ReplayModSimplePathing implements PathingRegistry { + public static final String MOD_ID = "replaymod-simplepathing"; + + @Mod.Instance(ReplayMod.MOD_ID) + private static ReplayMod core; + + private Logger logger; + + @Mod.EventHandler + public void preInit(FMLPreInitializationEvent event) { + logger = event.getModLog(); + + core.getSettingsRegistry().register(Setting.class); + + FMLCommonHandler.instance().bus().register(this); + } + + @SubscribeEvent + public void postReplayOpen(ReplayOpenEvent.Post event) { + new GuiPathing(core, this, event.getReplayHandler()); + } + + private Timeline currentTimeline = createTimeline(); { currentTimeline.createPath(); } + private Keyframe selectedKeyframe; + + public Keyframe getSelectedKeyframe() { + return selectedKeyframe; + } + + public void setSelectedKeyframe(Keyframe selectedKeyframe) { + this.selectedKeyframe = selectedKeyframe; + } + + public void setCurrentTimeline(Timeline currentTimeline) { + this.currentTimeline = currentTimeline; + } + + public Timeline getCurrentTimeline() { + return currentTimeline; + } + + @Override + public Timeline createTimeline() { + Timeline timeline = new TimelineImpl(); + + timeline.registerProperty(TimestampProperty.PROPERTY); + timeline.registerProperty(CameraProperties.POSITION); + timeline.registerProperty(CameraProperties.ROTATION); + + return timeline; + } + + @Override + public void serializeInterpolator(JsonWriter writer, Interpolator interpolator) throws IOException { + if (interpolator instanceof LinearInterpolator) { + writer.value("linear"); + } else { + throw new IOException("Unknown interpolator type: " + interpolator); + } + } + + @Override + public Interpolator deserializeInterpolator(JsonReader reader) throws IOException { + AbstractInterpolator interpolator; + String type = reader.nextString(); + switch (type) { + case "linear": + interpolator = new LinearInterpolator(); + interpolator.registerProperty(TimestampProperty.PROPERTY); + interpolator.registerProperty(CameraProperties.POSITION); + interpolator.registerProperty(CameraProperties.ROTATION); + return interpolator; + default: + throw new IOException("Unknown interpolation type: " + type); + + } + } +} diff --git a/src/main/java/com/replaymod/simplepathing/Setting.java b/src/main/java/com/replaymod/simplepathing/Setting.java new file mode 100644 index 00000000..720331a3 --- /dev/null +++ b/src/main/java/com/replaymod/simplepathing/Setting.java @@ -0,0 +1,17 @@ +package com.replaymod.simplepathing; + +import com.replaymod.core.SettingsRegistry; + +public final class Setting extends SettingsRegistry.SettingKeys { +// public static final Setting RECORD_SINGLEPLAYER = make("recordSingleplayer", "recordsingleplayer", true); +// public static final Setting RECORD_SERVER = make("recordServer", "recordserver", true); +// public static final Setting INDICATOR = make("indicator", "indicator", true); + + private static Setting make(String key, String displayName, T defaultValue) { + return new Setting<>(key, displayName, defaultValue); + } + + public Setting(String key, String displayString, T defaultValue) { + super("simplepathing", key, "replaymod.gui.settings." + displayString, defaultValue); + } +} diff --git a/src/main/java/com/replaymod/simplepathing/gui/GuiKeyframeTimeline.java b/src/main/java/com/replaymod/simplepathing/gui/GuiKeyframeTimeline.java new file mode 100644 index 00000000..f208716b --- /dev/null +++ b/src/main/java/com/replaymod/simplepathing/gui/GuiKeyframeTimeline.java @@ -0,0 +1,58 @@ +package com.replaymod.simplepathing.gui; + +import com.replaymod.core.ReplayMod; +import com.replaymod.pathing.path.Keyframe; +import com.replaymod.pathing.properties.CameraProperties; +import com.replaymod.pathing.properties.TimestampProperty; +import com.replaymod.simplepathing.ReplayModSimplePathing; +import de.johni0702.minecraft.gui.GuiRenderer; +import de.johni0702.minecraft.gui.element.advanced.AbstractGuiTimeline; +import org.lwjgl.util.ReadableDimension; + +public class GuiKeyframeTimeline extends AbstractGuiTimeline { + protected static final int KEYFRAME_SIZE = 5; + protected static final int KEYFRAME_TEXTURE_X = 74; + protected static final int KEYFRAME_TEXTURE_Y = 20; + + private final GuiPathing gui; + + public GuiKeyframeTimeline(GuiPathing gui) { + this.gui = gui; + } + + @Override + protected void drawTimelineCursor(GuiRenderer renderer, ReadableDimension size) { + ReplayModSimplePathing mod = gui.getMod(); + + int width = size.getWidth(); + int visibleWidth = width - BORDER_LEFT - BORDER_RIGHT; + int startTime = getOffset(); + int visibleTime = (int) (getZoom() * getLength()); + int endTime = getOffset() + visibleTime; + + renderer.bindTexture(ReplayMod.TEXTURE); + + for (Keyframe keyframe : mod.getCurrentTimeline().getPaths().get(0).getKeyframes()) { + 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 v = KEYFRAME_TEXTURE_Y; + if (keyframe.getValue(CameraProperties.POSITION).isPresent()) { + renderer.drawTexturedRect(positonX, BORDER_TOP, u, v, KEYFRAME_SIZE, KEYFRAME_SIZE); + } + if (keyframe.getValue(TimestampProperty.PROPERTY).isPresent()) { + v += KEYFRAME_SIZE; + renderer.drawTexturedRect(positonX, BORDER_TOP + KEYFRAME_SIZE, u, v, KEYFRAME_SIZE, KEYFRAME_SIZE); + } + } + } + + super.drawTimelineCursor(renderer, size); + } + + @Override + protected GuiKeyframeTimeline 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 new file mode 100644 index 00000000..a27d0ed9 --- /dev/null +++ b/src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java @@ -0,0 +1,346 @@ +package com.replaymod.simplepathing.gui; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.replaymod.core.ReplayMod; +import com.replaymod.pathing.change.AddKeyframe; +import com.replaymod.pathing.change.CombinedChange; +import com.replaymod.pathing.change.UpdateKeyframeProperties; +import com.replaymod.pathing.gui.GuiKeyframeRepository; +import com.replaymod.pathing.interpolation.LinearInterpolator; +import com.replaymod.pathing.path.Keyframe; +import com.replaymod.pathing.path.Path; +import com.replaymod.pathing.path.PathSegment; +import com.replaymod.pathing.path.Timeline; +import com.replaymod.pathing.player.RealtimeTimelinePlayer; +import com.replaymod.pathing.properties.CameraProperties; +import com.replaymod.pathing.properties.TimestampProperty; +import com.replaymod.replay.ReplayHandler; +import com.replaymod.replay.camera.CameraEntity; +import com.replaymod.replay.gui.overlay.GuiReplayOverlay; +import com.replaymod.simplepathing.ReplayModSimplePathing; +import de.johni0702.minecraft.gui.GuiRenderer; +import de.johni0702.minecraft.gui.RenderInfo; +import de.johni0702.minecraft.gui.container.GuiPanel; +import de.johni0702.minecraft.gui.element.*; +import de.johni0702.minecraft.gui.element.advanced.GuiTimelineTime; +import de.johni0702.minecraft.gui.element.advanced.IGuiTimeline; +import de.johni0702.minecraft.gui.layout.CustomLayout; +import de.johni0702.minecraft.gui.layout.HorizontalLayout; +import de.johni0702.minecraft.gui.layout.VerticalLayout; +import org.apache.commons.lang3.tuple.Triple; +import org.lwjgl.input.Keyboard; +import org.lwjgl.util.Dimension; +import org.lwjgl.util.ReadableDimension; +import org.lwjgl.util.ReadablePoint; +import org.lwjgl.util.WritablePoint; + +import java.io.IOException; + + +/** + * Gui plug-in to the GuiReplayOverlay for simple pathing. + */ +public class GuiPathing { + public final GuiTexturedButton playPauseButton = new GuiTexturedButton().setSize(20, 20) + .setTexture(ReplayMod.TEXTURE, ReplayMod.TEXTURE_SIZE); + + public final GuiTexturedButton positionKeyframeButton = new GuiTexturedButton().setSize(20, 20) + .setTexture(ReplayMod.TEXTURE, ReplayMod.TEXTURE_SIZE); + + public final GuiTexturedButton timeKeyframeButton = new GuiTexturedButton().setSize(20, 20) + .setTexture(ReplayMod.TEXTURE, ReplayMod.TEXTURE_SIZE); + + public final GuiKeyframeTimeline timeline = new GuiKeyframeTimeline(this){ + @Override + public void draw(GuiRenderer renderer, ReadableDimension size, RenderInfo renderInfo) { + if (player.isActive()) { + setCursorPosition((int) player.getTimePassed()); + } + super.draw(renderer, size, renderInfo); + } + }.onClick(new IGuiTimeline.OnClick() { + @Override + public void run(int time) { + timeline.setCursorPosition(time); + // TODO: Keyframe selection + mod.setSelectedKeyframe(null); + } + }).setSize(Integer.MAX_VALUE, 20).setLength(30 * 60 * 1000).setMarkers(); + + public final GuiHorizontalScrollbar scrollbar = new GuiHorizontalScrollbar().setSize(Integer.MAX_VALUE, 9); + {scrollbar.onValueChanged(new Runnable() { + @Override + public void run() { + timeline.setOffset((int) (scrollbar.getPosition() * timeline.getLength())); + timeline.setZoom(scrollbar.getZoom()); + } + }).setZoom(0.1);} + + public final GuiTimelineTime timelineTime = new GuiTimelineTime() + .setTimeline(timeline); + + public final GuiTexturedButton zoomInButton = new GuiTexturedButton().setSize(9, 9).onClick(new Runnable() { + @Override + public void run() { + zoomTimeline(2d / 3d); + } + }).setTexture(ReplayMod.TEXTURE, ReplayMod.TEXTURE_SIZE).setTexturePosH(40, 20); + + public final GuiTexturedButton zoomOutButton = new GuiTexturedButton().setSize(9, 9).onClick(new Runnable() { + @Override + public void run() { + zoomTimeline(3d / 2d); + } + }).setTexture(ReplayMod.TEXTURE, ReplayMod.TEXTURE_SIZE).setTexturePosH(40, 30); + + public final GuiPanel zoomButtonPanel = new GuiPanel() + .setLayout(new VerticalLayout(VerticalLayout.Alignment.CENTER).setSpacing(2)) + .addElements(null, zoomInButton, zoomOutButton); + + public final GuiPanel timelinePanel = new GuiPanel().setSize(Integer.MAX_VALUE, 40) + .setLayout(new CustomLayout() { + @Override + protected void layout(GuiPanel container, int width, int height) { + pos(zoomButtonPanel, width - width(zoomButtonPanel), 10); + pos(timelineTime, 0, 2); + size(timelineTime, x(zoomButtonPanel), 8); + pos(timeline, 0, y(timelineTime) + height(timelineTime)); + size(timeline, x(zoomButtonPanel) - 2, 20); + pos(scrollbar, 0, y(timeline) + height(timeline) + 1); + size(scrollbar, x(zoomButtonPanel) - 2, 9); + } + }).addElements(null, timelineTime, timeline, scrollbar, zoomButtonPanel); + + public final GuiPanel panel = new GuiPanel() + .setLayout(new HorizontalLayout(HorizontalLayout.Alignment.CENTER).setSpacing(5)) + .addElements(new HorizontalLayout.Data(0.5), playPauseButton, positionKeyframeButton, timeKeyframeButton, + timelinePanel); + + /** + * IGuiClickable dummy component that is inserted at a high level. + * During path playback, this catches all click events and forwards them to the + * abort path playback button. + * Dragging does not have to be intercepted as every GUI element should only + * respond to dragging events after it has received and handled a click event. + */ + private final IGuiClickable clickCatcher = new AbstractGuiClickable() { + @Override + protected AbstractGuiElement getThis() { + return this; + } + + @Override + protected ReadableDimension calcMinSize() { + return new Dimension(0, 0); + } + + @Override + public boolean mouseClick(ReadablePoint position, int button) { + if (player.isActive()) { + playPauseButton.mouseClick(position, button); + return true; + } + return false; + } + + @Override + public int getLayer() { + return player.isActive() ? 10 : 0; + } + }; + + private final ReplayModSimplePathing mod; + private final ReplayHandler replayHandler; + private final RealtimeTimelinePlayer player; + + public GuiPathing(final ReplayMod core, final ReplayModSimplePathing mod, final ReplayHandler replayHandler) { + this.mod = mod; + this.replayHandler = replayHandler; + this.player = new RealtimeTimelinePlayer(replayHandler); + final GuiReplayOverlay overlay = replayHandler.getOverlay(); + + playPauseButton.setTexturePosH(new ReadablePoint() { + @Override + public int getX() { + return 0; + } + + @Override + public int getY() { + return player.isActive() ? 20 : 0; + } + + @Override + public void getLocation(WritablePoint dest) { + dest.setLocation(getX(), getY()); + } + }).onClick(new Runnable() { + @Override + public void run() { + if (player.isActive()) { + player.getFuture().cancel(false); + } else { + Timeline timeline = mod.getCurrentTimeline(); + Path path = timeline.getPaths().get(0); + + // TODO Change interpolator via Change class + LinearInterpolator interpolator = new LinearInterpolator(); + interpolator.registerProperty(TimestampProperty.PROPERTY); + interpolator.registerProperty(CameraProperties.POSITION); + interpolator.registerProperty(CameraProperties.ROTATION); + for (PathSegment segment : path.getSegments()) { + segment.setInterpolator(interpolator); + } + path.updateAll(); + + player.start(timeline); + } + } + }); + + positionKeyframeButton.setTexturePosH(new ReadablePoint() { + @Override + public int getX() { + return 0; + } + + @Override + public int getY() { + Keyframe keyframe = mod.getSelectedKeyframe(); + boolean present = keyframe != null + && keyframe.getValue(CameraProperties.POSITION).isPresent(); + return present ? 60 : 40; + } + + @Override + public void getLocation(WritablePoint dest) { + dest.setLocation(getX(), getY()); + } + }).onClick(new Runnable() { + @Override + public void run() { + updateKeyframe(false); + } + }); + + timeKeyframeButton.setTexturePosH(new ReadablePoint() { + @Override + public int getX() { + return 0; + } + + @Override + public int getY() { + Keyframe keyframe = mod.getSelectedKeyframe(); + boolean present = keyframe != null + && keyframe.getValue(TimestampProperty.PROPERTY).isPresent(); + return present ? 100 : 80; + } + + @Override + public void getLocation(WritablePoint dest) { + dest.setLocation(getX(), getY()); + } + }).onClick(new Runnable() { + @Override + public void run() { + updateKeyframe(true); + } + }); + + overlay.addElements(null, panel, clickCatcher); + overlay.setLayout(new CustomLayout(overlay.getLayout()) { + @Override + protected void layout(GuiReplayOverlay container, int width, int height) { + pos(panel, 10, y(overlay.topPanel) + height(overlay.topPanel) + 3); + size(panel, width - 20, 40); + size(clickCatcher, 0, 0); + } + }); + + core.getKeyBindingRegistry().registerKeyBinding("replaymod.input.keyframerepository", Keyboard.KEY_X, new Runnable() { + @Override + public void run() { + if (!overlay.isVisible()) { + return; + } + try { + GuiKeyframeRepository gui = new GuiKeyframeRepository( + mod, replayHandler.getReplayFile(), mod.getCurrentTimeline()); + Futures.addCallback(gui.getFuture(), new FutureCallback() { + @Override + public void onSuccess(Timeline result) { + if (result != null) { + mod.setCurrentTimeline(result); + } + } + + @Override + public void onFailure(Throwable t) { + t.printStackTrace(); + core.printWarningToChat("Error loading timeline: " + t.getMessage()); + } + }); + gui.display(); + } catch (IOException e) { + e.printStackTrace(); + core.printWarningToChat("Error loading timeline: " + e.getMessage()); + } + } + }); + } + + public void zoomTimeline(double factor) { + 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) { + int time = timeline.getCursorPosition(); + Timeline timeline = mod.getCurrentTimeline(); + Path path = timeline.getPaths().get(0); + + AddKeyframe addChange = null; + Keyframe keyframe = path.getKeyframe(time); + if (keyframe == null) { + addChange = AddKeyframe.create(path, time); + addChange.apply(timeline); + keyframe = path.getKeyframe(time); + } + + UpdateKeyframeProperties.Builder builder = UpdateKeyframeProperties.create(path, keyframe); + if (isTime) { + if (keyframe.getValue(TimestampProperty.PROPERTY).isPresent()) { + builder.removeProperty(TimestampProperty.PROPERTY); + } else { + builder.setValue(TimestampProperty.PROPERTY, replayHandler.getReplaySender().currentTimeStamp()); + } + } else { + if (keyframe.getValue(CameraProperties.POSITION).isPresent()) { + builder.removeProperty(CameraProperties.POSITION); + builder.removeProperty(CameraProperties.ROTATION); + } 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)); + } + } + UpdateKeyframeProperties updateChange = builder.done(); + updateChange.apply(timeline); + if (addChange == null) { + timeline.pushChange(updateChange); + } else { + timeline.pushChange(CombinedChange.createFromApplied(addChange, updateChange)); + } + + mod.setSelectedKeyframe(keyframe); + } + + public ReplayModSimplePathing getMod() { + return mod; + } +} diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/GuiEditKeyframe.java b/src/main/java/eu/crushedpixel/replaymod/gui/GuiEditKeyframe.java index ee8edb81..ddae78cc 100644 --- a/src/main/java/eu/crushedpixel/replaymod/gui/GuiEditKeyframe.java +++ b/src/main/java/eu/crushedpixel/replaymod/gui/GuiEditKeyframe.java @@ -153,7 +153,7 @@ public abstract class GuiEditKeyframe extends GuiScreen public void onGuiClosed() { // TODO // if(!save) { -// ReplayHandler.removeKeyframe(keyframe); +// ReplayHandler.removeKeyframe(property); // ReplayHandler.addKeyframe(keyframeBackup); // ReplayHandler.selectKeyframe(keyframeBackup); // } @@ -218,8 +218,8 @@ public abstract class GuiEditKeyframe extends GuiScreen // private static class GuiEditKeyframeMarker extends GuiEditKeyframe { // private GuiAdvancedTextField markerNameInput; // -// public GuiEditKeyframeMarker(Keyframe keyframe) { -// super(keyframe, ReplayHandler.getMarkerKeyframes()); +// public GuiEditKeyframeMarker(Keyframe property) { +// super(property, ReplayHandler.getMarkerKeyframes()); // } // // @Override @@ -227,7 +227,7 @@ public abstract class GuiEditKeyframe extends GuiScreen // super.initGui(); // // if (!initialized) { -// String name = keyframe.getValue().getName(); +// String name = property.getValue().getName(); // if (name == null) name = ""; // markerNameInput = new GuiAdvancedTextField(fontRendererObj, 0, 0, 200, 20); // markerNameInput.hint = I18n.format("replaymod.gui.editkeyframe.markername"); @@ -246,7 +246,7 @@ public abstract class GuiEditKeyframe extends GuiScreen // // @Override // public void onGuiClosed() { -// keyframe.getValue().setName(markerNameInput.getText().trim()); +// property.getValue().setName(markerNameInput.getText().trim()); // super.onGuiClosed(); // } // } @@ -254,8 +254,8 @@ public abstract class GuiEditKeyframe extends GuiScreen // private static class GuiEditKeyframeTime extends GuiEditKeyframe { // private GuiNumberInput kfMin, kfSec, kfMs; // -// public GuiEditKeyframeTime(Keyframe keyframe) { -// super(keyframe, ReplayHandler.getTimeKeyframes()); +// public GuiEditKeyframeTime(Keyframe property) { +// super(property, ReplayHandler.getTimeKeyframes()); // screenTitle = I18n.format("replaymod.gui.editkeyframe.title.time"); // } // @@ -264,7 +264,7 @@ public abstract class GuiEditKeyframe extends GuiScreen // super.initGui(); // // if (!initialized) { -// int time = keyframe.getValue().asInt(); +// int time = property.getValue().asInt(); // // kfMin = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 30, 0d, null, (double)TimestampUtils.timestampToWholeMinutes(time), false, "", 1); // kfSec = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 25, 0d, 59d, (double)TimestampUtils.getSecondsFromTimestamp(time), false, "", 1); @@ -277,7 +277,7 @@ public abstract class GuiEditKeyframe extends GuiScreen // KeyframeValueChangeListener listener = new KeyframeValueChangeListener(this) { // @Override // public void onValueChange(double value) { -// keyframe.setValue(new TimestampValue(TimestampUtils.calculateTimestamp( +// property.setValue(new TimestampValue(TimestampUtils.calculateTimestamp( // kfMin.getIntValue(), kfSec.getIntValue(), kfMs.getIntValue()))); // // super.onValueChange(value); @@ -312,8 +312,8 @@ public abstract class GuiEditKeyframe extends GuiScreen // private GuiNumberInput xCoord, yCoord, zCoord, pitch, yaw, roll; // private ComposedElement posInputs; // -// public GuiEditKeyframePosition(Keyframe keyframe) { -// super(keyframe, ReplayHandler.getPositionKeyframes()); +// public GuiEditKeyframePosition(Keyframe property) { +// super(property, ReplayHandler.getPositionKeyframes()); // screenTitle = I18n.format("replaymod.gui.editkeyframe.title.pos"); // } // @@ -322,7 +322,7 @@ public abstract class GuiEditKeyframe extends GuiScreen // super.initGui(); // // if (!initialized) { -// AdvancedPosition pos = keyframe.getValue(); +// AdvancedPosition pos = property.getValue(); // xCoord = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 100, null, null, RoundUtils.round2Decimals(pos.getX()), true); // yCoord = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 100, null, null, RoundUtils.round2Decimals(pos.getY()), true); // zCoord = new GuiDraggingNumberInput(fontRendererObj, 0, 0, 100, null, null, RoundUtils.round2Decimals(pos.getZ()), true); @@ -336,7 +336,7 @@ public abstract class GuiEditKeyframe extends GuiScreen // KeyframeValueChangeListener listener = new KeyframeValueChangeListener(this) { // @Override // public void onValueChange(double value) { -// keyframe.setValue(new AdvancedPosition(xCoord.getPreciseValue(), yCoord.getPreciseValue(), +// property.setValue(new AdvancedPosition(xCoord.getPreciseValue(), yCoord.getPreciseValue(), // zCoord.getPreciseValue(), new Float(pitch.getPreciseValue()), (float) yaw.getPreciseValue(), // (float) roll.getPreciseValue())); // @@ -378,7 +378,7 @@ public abstract class GuiEditKeyframe extends GuiScreen // // @Override // protected void drawScreen0() { -// if (keyframe.getValue() instanceof SpectatorData) return; +// if (property.getValue() instanceof SpectatorData) return; // drawString(fontRendererObj, I18n.format("replaymod.gui.editkeyframe.xpos"), left, virtualY + 27, Color.WHITE.getRGB()); // drawString(fontRendererObj, I18n.format("replaymod.gui.editkeyframe.ypos"), left, virtualY + 57, Color.WHITE.getRGB()); // drawString(fontRendererObj, I18n.format("replaymod.gui.editkeyframe.zpos"), left, virtualY + 87, Color.WHITE.getRGB()); @@ -413,8 +413,8 @@ public abstract class GuiEditKeyframe extends GuiScreen // } // }; // -// public GuiEditKeyframeSpectator(Keyframe keyframe) { -// super(keyframe, ReplayHandler.getPositionKeyframes()); +// public GuiEditKeyframeSpectator(Keyframe property) { +// super(property, ReplayHandler.getPositionKeyframes()); // screenTitle = I18n.format("replaymod.gui.editkeyframe.title.spec"); // } // @@ -423,7 +423,7 @@ public abstract class GuiEditKeyframe extends GuiScreen // super.initGui(); // // if (!initialized) { -// SpectatorData data = (SpectatorData)keyframe.getValue(); +// SpectatorData data = (SpectatorData)property.getValue(); // // perspectiveButton = new GuiToggleButton(0, 0, 0, 200, 20, I18n.format("replaymod.gui.editkeyframe.spec.method")+": ", new String[]{ // I18n.format("replaymod.gui.editkeyframe.spec.method.firstperson"), @@ -481,7 +481,7 @@ public abstract class GuiEditKeyframe extends GuiScreen // // @Override // public void onGuiClosed() { -// SpectatorData data = (SpectatorData)keyframe.getValue(); +// SpectatorData data = (SpectatorData)property.getValue(); // data.setSpectatingMethod(SpectatorData.SpectatingMethod.values()[perspectiveButton.getValue()]); // data.getThirdPersonInfo().shoulderCamDistance = shoulderDistanceInput.getPreciseValue(); // data.getThirdPersonInfo().shoulderCamPitchOffset = shoulderPitchOffsetInput.getIntValue(); @@ -506,7 +506,7 @@ public abstract class GuiEditKeyframe extends GuiScreen // @Override // public void onValueChange(double value) { // int realTimestamp = TimestampUtils.calculateTimestamp(parent.min.getIntValue(), parent.sec.getIntValue(), parent.ms.getIntValue()); -// parent.keyframe.setRealTimestamp(realTimestamp); +// parent.property.setRealTimestamp(realTimestamp); // // ReplayHandler.fireKeyframesModifyEvent(); // } diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/GuiObjectManager.java b/src/main/java/eu/crushedpixel/replaymod/gui/GuiObjectManager.java index fe8156e1..f291527c 100644 --- a/src/main/java/eu/crushedpixel/replaymod/gui/GuiObjectManager.java +++ b/src/main/java/eu/crushedpixel/replaymod/gui/GuiObjectManager.java @@ -459,7 +459,7 @@ public class GuiObjectManager extends GuiScreen { // public void onValueChange(double value) { // NumberValue numberValue = new NumberValue(value); // -// //if keyframe selected, overwrite its value +// //if property selected, overwrite its value // if(toModify.contains(objectKeyframeTimeline.getSelectedKeyframe())) { // objectKeyframeTimeline.getSelectedKeyframe().setValue(numberValue); // } else { @@ -488,7 +488,7 @@ public class GuiObjectManager extends GuiScreen { // public void onValueChange(double value) { // Position position = new Position(xInput.getPreciseValue(), yInput.getPreciseValue(), zInput.getPreciseValue()); // -// //if keyframe selected, overwrite its value +// //if property selected, overwrite its value // if(toModify.contains(objectKeyframeTimeline.getSelectedKeyframe())) { // objectKeyframeTimeline.getSelectedKeyframe().setValue(position); // } else { diff --git a/src/main/java/eu/crushedpixel/replaymod/gui/elements/timelines/GuiKeyframeTimeline.java b/src/main/java/eu/crushedpixel/replaymod/gui/elements/timelines/GuiKeyframeTimeline.java index 648056fb..3f2c416a 100644 --- a/src/main/java/eu/crushedpixel/replaymod/gui/elements/timelines/GuiKeyframeTimeline.java +++ b/src/main/java/eu/crushedpixel/replaymod/gui/elements/timelines/GuiKeyframeTimeline.java @@ -47,7 +47,7 @@ public class GuiKeyframeTimeline extends GuiTimeline { // // //left mouse button // if(button == 0) { -// ReplayHandler.selectKeyframe(closest); //can be null, deselects keyframe +// ReplayHandler.selectKeyframe(closest); //can be null, deselects property // // // If we clicked on a key frame, then continue monitoring the mouse for movements // long currentTime = System.currentTimeMillis(); @@ -241,7 +241,7 @@ public class GuiKeyframeTimeline extends GuiTimeline { // textureY = KEYFRAME_TIME_Y; // y += 5; // } else { -// throw new UnsupportedOperationException("Unknown keyframe type: " + kf.getClass()); +// throw new UnsupportedOperationException("Unknown property type: " + kf.getClass()); // } // // if (ReplayHandler.isSelected(kf)) { diff --git a/src/main/java/eu/crushedpixel/replaymod/interpolation/AdvancedPositionKeyframeList.java b/src/main/java/eu/crushedpixel/replaymod/interpolation/AdvancedPositionKeyframeList.java index a1d03c49..110c2d77 100644 --- a/src/main/java/eu/crushedpixel/replaymod/interpolation/AdvancedPositionKeyframeList.java +++ b/src/main/java/eu/crushedpixel/replaymod/interpolation/AdvancedPositionKeyframeList.java @@ -58,11 +58,11 @@ public class AdvancedPositionKeyframeList extends KeyframeList // TimestampValue timestampValue = ReplayHandler.getTimeKeyframes().getInterpolatedValueForTimestamp(keyframeTimestamp, true); // int replayTimestamp = timestampValue == null ? 0 : timestampValue.asInt(); // if(firstTimestamp == -1) firstTimestamp = replayTimestamp; -// spectatorInterpolation.addPoint(keyframe, replayTimestamp); +// spectatorInterpolation.addPoint(property, replayTimestamp); // if(iterator.hasNext()) { -// keyframe = iterator.next(); -// found = keyframe.getValue() instanceof SpectatorData; -// if(!found) completedKeyframes.add(keyframe); +// property = iterator.next(); +// found = property.getValue() instanceof SpectatorData; +// if(!found) completedKeyframes.add(property); // } else { // found = false; // } diff --git a/src/main/java/eu/crushedpixel/replaymod/interpolation/SpectatorDataInterpolation.java b/src/main/java/eu/crushedpixel/replaymod/interpolation/SpectatorDataInterpolation.java index bf85f566..a5521ec5 100644 --- a/src/main/java/eu/crushedpixel/replaymod/interpolation/SpectatorDataInterpolation.java +++ b/src/main/java/eu/crushedpixel/replaymod/interpolation/SpectatorDataInterpolation.java @@ -54,7 +54,7 @@ public class SpectatorDataInterpolation { } thirdPersonInfoInterpolation.prepare(); - //updating the spectator keyframe's position in the world to smoothly continue the path + //updating the spectator property's position in the world to smoothly continue the path //with non-spectator position keyframes for(Pair> pair : points) { // TODO @@ -74,7 +74,7 @@ public class SpectatorDataInterpolation { // pair.getValue().getValue().apply(entityPosition); } - //feed the underlying keyframe list with AdvancedPosition Keyframes that are derived from the Spectator Keyframes + //feed the underlying property list with AdvancedPosition Keyframes that are derived from the Spectator Keyframes underlyingKeyframes = new KeyframeList(); int i = 0; int firstTimestamp = -1; diff --git a/src/main/java/eu/crushedpixel/replaymod/registry/KeybindRegistry.java b/src/main/java/eu/crushedpixel/replaymod/registry/KeybindRegistry.java index f06a7766..79445bb7 100755 --- a/src/main/java/eu/crushedpixel/replaymod/registry/KeybindRegistry.java +++ b/src/main/java/eu/crushedpixel/replaymod/registry/KeybindRegistry.java @@ -40,7 +40,6 @@ public class KeybindRegistry { replayModKeyBindings.add(new KeyBinding(KEY_RESET_TILT, Keyboard.KEY_K, "replaymod.title")); replayModKeyBindings.add(new KeyBinding(KEY_CLEAR_KEYFRAMES, Keyboard.KEY_C, "replaymod.title")); - replayModKeyBindings.add(new KeyBinding(KEY_KEYFRAME_PRESETS, Keyboard.KEY_X, "replaymod.title")); replayModKeyBindings.add(new KeyBinding(KEY_PATH_PREVIEW, Keyboard.KEY_H, "replaymod.title")); replayModKeyBindings.add(new KeyBinding(KEY_SYNC_TIMELINE, Keyboard.KEY_V, "replaymod.title")); diff --git a/src/main/java/eu/crushedpixel/replaymod/renderer/PathPreviewRenderer.java b/src/main/java/eu/crushedpixel/replaymod/renderer/PathPreviewRenderer.java index 31ffbf1e..7dce53f3 100644 --- a/src/main/java/eu/crushedpixel/replaymod/renderer/PathPreviewRenderer.java +++ b/src/main/java/eu/crushedpixel/replaymod/renderer/PathPreviewRenderer.java @@ -55,8 +55,8 @@ public class PathPreviewRenderer { // AdvancedPosition previousPosition = null; // // int i = 0; -// for(Keyframe keyframe : keyframes) { -// int timestamp = keyframe.getRealTimestamp(); +// for(Keyframe property : keyframes) { +// int timestamp = property.getRealTimestamp(); // int nextTimestamp = timestamp; // // if(i+1 < keyframes.size()) { diff --git a/src/main/java/eu/crushedpixel/replaymod/replay/ReplayHandler.java b/src/main/java/eu/crushedpixel/replaymod/replay/ReplayHandler.java index 05e6243c..4d839760 100755 --- a/src/main/java/eu/crushedpixel/replaymod/replay/ReplayHandler.java +++ b/src/main/java/eu/crushedpixel/replaymod/replay/ReplayHandler.java @@ -168,38 +168,38 @@ public class ReplayHandler { // // TODO // } // -// public static void addTimeKeyframe(Keyframe keyframe) { -// timeKeyframes.add(keyframe); -// selectKeyframe(keyframe); +// public static void addTimeKeyframe(Keyframe property) { +// timeKeyframes.add(property); +// selectKeyframe(property); // // fireKeyframesModifyEvent(); // } // -// public static void addPositionKeyframe(Keyframe keyframe) { -// positionKeyframes.add(keyframe); -// selectKeyframe(keyframe); +// public static void addPositionKeyframe(Keyframe property) { +// positionKeyframes.add(property); +// selectKeyframe(property); // // fireKeyframesModifyEvent(); // } // // @SuppressWarnings("unchecked") -// public static void addKeyframe(Keyframe keyframe) { -// if(keyframe.getValue() instanceof AdvancedPosition) { -// addPositionKeyframe(keyframe); -// } else if(keyframe.getValue() instanceof TimestampValue) { -// addTimeKeyframe(keyframe); +// public static void addKeyframe(Keyframe property) { +// if(property.getValue() instanceof AdvancedPosition) { +// addPositionKeyframe(property); +// } else if(property.getValue() instanceof TimestampValue) { +// addTimeKeyframe(property); // } // } // -// public static void removeKeyframe(Keyframe keyframe) { -// if(keyframe.getValue() instanceof AdvancedPosition) { -// positionKeyframes.remove(keyframe); -// } else if(keyframe.getValue() instanceof TimestampValue) { -// timeKeyframes.remove(keyframe); +// public static void removeKeyframe(Keyframe property) { +// if(property.getValue() instanceof AdvancedPosition) { +// positionKeyframes.remove(property); +// } else if(property.getValue() instanceof TimestampValue) { +// timeKeyframes.remove(property); // } // // TODO Marker // -// if(keyframe == selectedKeyframe) { +// if(property == selectedKeyframe) { // selectKeyframe(null); // } // @@ -485,14 +485,14 @@ public class ReplayHandler { // // int prevTime, prevRealTime; // -// Keyframe keyframe = timeKeyframes.last(); +// Keyframe property = timeKeyframes.last(); // -// if(keyframe == null) { +// if(property == null) { // prevTime = 0; // prevRealTime = 0; // } else { -// prevTime = (int)keyframe.getValue().value; -// prevRealTime = keyframe.getRealTimestamp(); +// prevTime = (int)property.getValue().value; +// prevRealTime = property.getRealTimestamp(); // } // // double speed = ignoreReplaySpeed ? 1 : ReplayMod.overlay.getSpeedSliderValue(); diff --git a/src/main/java/eu/crushedpixel/replaymod/studio/StudioImplementation.java b/src/main/java/eu/crushedpixel/replaymod/studio/StudioImplementation.java index b11589fa..3da21537 100755 --- a/src/main/java/eu/crushedpixel/replaymod/studio/StudioImplementation.java +++ b/src/main/java/eu/crushedpixel/replaymod/studio/StudioImplementation.java @@ -4,6 +4,7 @@ import com.google.common.base.Optional; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonObject; +import com.replaymod.pathing.serialize.LegacyKeyframeSetAdapter; import de.johni0702.replaystudio.PacketData; import de.johni0702.replaystudio.filter.ChangeTimestampFilter; import de.johni0702.replaystudio.filter.NeutralizerFilter; @@ -19,7 +20,6 @@ import eu.crushedpixel.replaymod.gui.elements.listeners.ProgressUpdateListener; import eu.crushedpixel.replaymod.holders.Keyframe; import eu.crushedpixel.replaymod.holders.KeyframeSet; import eu.crushedpixel.replaymod.holders.TimestampValue; -import eu.crushedpixel.replaymod.utils.LegacyKeyframeSetAdapter; import net.minecraft.client.resources.I18n; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.Validate; diff --git a/src/main/resources/assets/replaymod/lang/en_US.lang b/src/main/resources/assets/replaymod/lang/en_US.lang index 0ea58865..08c15a49 100644 --- a/src/main/resources/assets/replaymod/lang/en_US.lang +++ b/src/main/resources/assets/replaymod/lang/en_US.lang @@ -86,6 +86,9 @@ replaymod.gui.loading=Loading replaymod.gui.pleasewait=Please wait replaymod.gui.render=Render replaymod.gui.iphidden=Server IP Hidden +replaymod.gui.overwrite=Overwrite +replaymod.gui.saveas=Save as ... +replaymod.gui.done=Done replaymod.gui.original=Original replaymod.gui.modified=Modified @@ -101,6 +104,9 @@ replaymod.gui.pitch=Pitch replaymod.gui.yaw=Yaw replaymod.gui.roll=Roll +replaymod.gui.camera=Camera +replaymod.gui.position=Position + replaymod.gui.unknownerror=An unknown error occured replaymod.gui.exit=Exit Replay @@ -113,6 +119,8 @@ replaymod.gui.loggedout=LOGGED OUT replaymod.gui.mainmenu=Main Menu replaymod.gui.settings=Settings +replaymod.gui.keyframerepo.delete=Are you sure you want to delete these keyframes? + #Only change these if it's neccessary replaymod.gui.replayviewer=Replay Viewer replaymod.gui.replaycenter=Replay Center diff --git a/src/main/resources/mcmod.info b/src/main/resources/mcmod.info index 6e5330d7..1ebcdfd6 100755 --- a/src/main/resources/mcmod.info +++ b/src/main/resources/mcmod.info @@ -66,6 +66,23 @@ "screenshots": [], "dependencies": ["required-after:replaymod-replay"] }, + { + "modid": "replaymod-simplepathing", + "name": "Replay Mod - Simple Pathing", + "description": "Simple Pathing Module of the ReplayMod - aka the original pathing", + "version": "${version}", + "mcversion": "${mcversion}", + "url": "https://replaymod.com", + "updateUrl": "https://replaymod.com/download", + "authorList": [ + "CrushedPixel", + "johni0702" + ], + "logoFile": "replaymod_logo.png", + "parent": "replaymod", + "screenshots": [], + "dependencies": ["required-after:replaymod-replay"] + }, { "modid": "replaymod-extras", "name": "Replay Mod - Extras",