ed56673Merge branch '1.8' into 1.8.991573d9Update jGui, ReplayStudio and Translationsd71358bAdd confirmation dialog before overwriting path presets (fixes #65)1a55983Move addCallback method from integration test Utils to main Utils18c5bcdBe more cooperative with other mods on the ingame menu (fixes #42)c054fe8Register keys for simplepathing only once (fixes #63)f13297cFix default interpolator handling in keyframe gui and after loading (fixes #64)391f304Show error popup if entity tracker fails to load0e0eaaaFix entity tracker being set even if it failed loading (fixes #50)a34bbbcFix NPE when spectating a player without a camera entity60879fbChange local class to be an inner class because Srg2Source breaks with itdfafbecLoad translations from github repo, only reload language not all resource packs8a6ec51Add missing tooltips to player overview and various missing chat messages5677fc5Re-add warning to replay editor748a91eFix missing tooltips on fav/like/dislike buttons in replay centeree24866Fix upload replay button not being updated when name/tags changedba085cMove translations into separate repo3f0e3e7Fix NPE when receiving Replay|Restrict messages (fixes #16 GH)40e1d85Fix crash when saving the last keyframe in the edit gui (fixes #61)03aada1Update Mixin to 0.6.8 (fixes #9 GH)0c1dc65Fix interpolator being lost when moving keyframe to the end (fixes #62)fcbbbc9Add integration test. Run with ./gradlew runIntegrationTestfe6ded0Fix movement of keyframe via GuiEditKeyframe not updating selected keyframef58fa8fFix Login GUI not respecting other GUIs opened on startup4fc3a31Fix OpenEye being installed into working dir instead of mcDataDir
681 lines
30 KiB
Java
681 lines
30 KiB
Java
package com.replaymod.simplepathing;
|
|
|
|
import com.google.common.base.Preconditions;
|
|
import com.google.common.collect.Iterables;
|
|
import com.google.gson.JsonObject;
|
|
import com.google.gson.JsonParser;
|
|
import com.google.gson.stream.JsonReader;
|
|
import com.google.gson.stream.JsonWriter;
|
|
import com.replaymod.pathing.properties.CameraProperties;
|
|
import com.replaymod.pathing.properties.SpectatorProperty;
|
|
import com.replaymod.pathing.properties.TimestampProperty;
|
|
import com.replaymod.replaystudio.pathing.PathingRegistry;
|
|
import com.replaymod.replaystudio.pathing.change.AddKeyframe;
|
|
import com.replaymod.replaystudio.pathing.change.Change;
|
|
import com.replaymod.replaystudio.pathing.change.CombinedChange;
|
|
import com.replaymod.replaystudio.pathing.change.SetInterpolator;
|
|
import com.replaymod.replaystudio.pathing.change.UpdateKeyframeProperties;
|
|
import com.replaymod.replaystudio.pathing.impl.TimelineImpl;
|
|
import com.replaymod.replaystudio.pathing.interpolation.CatmullRomSplineInterpolator;
|
|
import com.replaymod.replaystudio.pathing.interpolation.CubicSplineInterpolator;
|
|
import com.replaymod.replaystudio.pathing.interpolation.Interpolator;
|
|
import com.replaymod.replaystudio.pathing.interpolation.LinearInterpolator;
|
|
import com.replaymod.replaystudio.pathing.path.Keyframe;
|
|
import com.replaymod.replaystudio.pathing.path.Path;
|
|
import com.replaymod.replaystudio.pathing.path.PathSegment;
|
|
import com.replaymod.replaystudio.pathing.path.Timeline;
|
|
import com.replaymod.replaystudio.pathing.property.Property;
|
|
import com.replaymod.replaystudio.util.EntityPositionTracker;
|
|
import com.replaymod.replaystudio.util.Location;
|
|
import com.replaymod.simplepathing.properties.ExplicitInterpolationProperty;
|
|
import lombok.Getter;
|
|
import net.minecraft.crash.CrashReport;
|
|
import net.minecraft.crash.CrashReportCategory;
|
|
import net.minecraft.util.ReportedException;
|
|
import org.apache.commons.lang3.ObjectUtils;
|
|
import org.apache.commons.lang3.Validate;
|
|
import org.apache.commons.lang3.tuple.Triple;
|
|
|
|
import java.io.ByteArrayOutputStream;
|
|
import java.io.IOException;
|
|
import java.io.PrintWriter;
|
|
import java.io.StringReader;
|
|
import java.util.ArrayList;
|
|
import java.util.Collection;
|
|
import java.util.Collections;
|
|
import java.util.HashMap;
|
|
import java.util.IdentityHashMap;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Optional;
|
|
import java.util.Set;
|
|
|
|
import static com.replaymod.replaystudio.pathing.change.RemoveKeyframe.create;
|
|
import static com.replaymod.simplepathing.ReplayModSimplePathing.LOGGER;
|
|
|
|
/**
|
|
* Simplified timeline abstraction used in the SimplePathing module.
|
|
*/
|
|
public class SPTimeline implements PathingRegistry {
|
|
public enum SPPath {
|
|
TIME,
|
|
POSITION,
|
|
}
|
|
|
|
@Getter
|
|
private final Timeline timeline;
|
|
@Getter
|
|
private final Path timePath;
|
|
@Getter
|
|
private final Path positionPath;
|
|
|
|
@Getter
|
|
private EntityPositionTracker entityTracker;
|
|
private InterpolatorType defaultInterpolatorType;
|
|
|
|
public SPTimeline() {
|
|
this(createInitialTimeline());
|
|
}
|
|
|
|
public SPTimeline(Timeline timeline) {
|
|
this.timeline = timeline;
|
|
this.timePath = timeline.getPaths().get(SPPath.TIME.ordinal());
|
|
this.positionPath = timeline.getPaths().get(SPPath.POSITION.ordinal());
|
|
}
|
|
|
|
public Path getPath(SPPath path) {
|
|
switch (path) {
|
|
case TIME:
|
|
return getTimePath();
|
|
case POSITION:
|
|
return getPositionPath();
|
|
}
|
|
throw new IllegalArgumentException("Unknown path " + path);
|
|
}
|
|
|
|
public Keyframe getKeyframe(SPPath path, long keyframe) {
|
|
return getPath(path).getKeyframe(keyframe);
|
|
}
|
|
|
|
public void setEntityTracker(EntityPositionTracker entityTracker) {
|
|
Preconditions.checkState(this.entityTracker == null, "Entity tracker already set");
|
|
this.entityTracker = entityTracker;
|
|
}
|
|
|
|
public void setDefaultInterpolatorType(InterpolatorType defaultInterpolatorType) {
|
|
Validate.isTrue(defaultInterpolatorType != InterpolatorType.DEFAULT, "Must not be DEFAULT");
|
|
this.defaultInterpolatorType = Validate.notNull(defaultInterpolatorType);
|
|
|
|
if (entityTracker != null) {
|
|
timeline.pushChange(updateInterpolators());
|
|
}
|
|
}
|
|
|
|
public Change setDefaultInterpolator(Interpolator interpolator) {
|
|
Preconditions.checkState(defaultInterpolatorType != null, "Default interpolator type not set.");
|
|
Validate.isInstanceOf(defaultInterpolatorType.getInterpolatorClass(), interpolator);
|
|
|
|
registerPositionInterpolatorProperties(interpolator);
|
|
|
|
Change change = CombinedChange.create(
|
|
positionPath.getSegments().stream()
|
|
// Ignore explicitly set segments
|
|
.filter(s -> !s.getStartKeyframe().getValue(ExplicitInterpolationProperty.PROPERTY).isPresent())
|
|
// Ignore spectator segments
|
|
.filter(s -> !isSpectatorSegment(s))
|
|
// Update interpolator for every remaining segment
|
|
// This will create a fragmented interpolator which is split by the updateInterpolators call
|
|
.map(s -> SetInterpolator.create(s, interpolator)).toArray(Change[]::new)
|
|
);
|
|
change.apply(timeline);
|
|
return CombinedChange.createFromApplied(change, updateInterpolators());
|
|
}
|
|
|
|
public boolean isTimeKeyframe(long time) {
|
|
return timePath.getKeyframe(time) != null;
|
|
}
|
|
|
|
public boolean isPositionKeyframe(long time) {
|
|
return positionPath.getKeyframe(time) != null;
|
|
}
|
|
|
|
public boolean isSpectatorKeyframe(long time) {
|
|
Keyframe keyframe = positionPath.getKeyframe(time);
|
|
return keyframe != null && keyframe.getValue(SpectatorProperty.PROPERTY).isPresent();
|
|
}
|
|
|
|
public void addPositionKeyframe(long time, double posX, double posY, double posZ,
|
|
float yaw, float pitch, float roll, int spectated) {
|
|
LOGGER.debug("Adding position keyframe at {} pos {}/{}/{} rot {}/{}/{} entId {}",
|
|
time, posX, posY, posZ, yaw, pitch, roll, spectated);
|
|
|
|
Path path = positionPath;
|
|
|
|
Preconditions.checkState(positionPath.getKeyframe(time) == null, "Keyframe already exists");
|
|
|
|
Change change = AddKeyframe.create(path, time);
|
|
change.apply(timeline);
|
|
Keyframe keyframe = path.getKeyframe(time);
|
|
|
|
UpdateKeyframeProperties.Builder builder = UpdateKeyframeProperties.create(path, keyframe);
|
|
builder.setValue(CameraProperties.POSITION, Triple.of(posX, posY, posZ));
|
|
builder.setValue(CameraProperties.ROTATION, Triple.of(yaw, pitch, roll));
|
|
if (spectated != -1) {
|
|
builder.setValue(SpectatorProperty.PROPERTY, spectated);
|
|
}
|
|
UpdateKeyframeProperties updateChange = builder.done();
|
|
updateChange.apply(timeline);
|
|
change = CombinedChange.createFromApplied(change, updateChange);
|
|
|
|
// If this new keyframe formed the first segment of the path
|
|
if (path.getSegments().size() == 1) {
|
|
// then create an initial interpolator of default type
|
|
PathSegment segment = path.getSegments().iterator().next();
|
|
Interpolator interpolator = createDefaultInterpolator();
|
|
SetInterpolator setInterpolator = SetInterpolator.create(segment, interpolator);
|
|
setInterpolator.apply(timeline);
|
|
change = CombinedChange.createFromApplied(change, setInterpolator);
|
|
}
|
|
|
|
// Update interpolators for spectator keyframes
|
|
// while this is overkill, it is far simpler than updating differently for every possible case
|
|
change = CombinedChange.createFromApplied(change, updateInterpolators());
|
|
|
|
Change specPosUpdate = updateSpectatorPositions();
|
|
specPosUpdate.apply(timeline);
|
|
change = CombinedChange.createFromApplied(change, specPosUpdate);
|
|
|
|
timeline.pushChange(change);
|
|
}
|
|
|
|
public Change updatePositionKeyframe(long time, double posX, double posY, double posZ,
|
|
float yaw, float pitch, float roll) {
|
|
LOGGER.debug("Updating position keyframe at {} to pos {}/{}/{} rot {}/{}/{}",
|
|
time, posX, posY, posZ, yaw, pitch, roll);
|
|
|
|
Keyframe keyframe = positionPath.getKeyframe(time);
|
|
|
|
Preconditions.checkState(keyframe != null, "Keyframe does not exists");
|
|
Preconditions.checkState(!keyframe.getValue(SpectatorProperty.PROPERTY).isPresent(), "Cannot update spectator keyframe");
|
|
|
|
Change change = UpdateKeyframeProperties.create(positionPath, keyframe)
|
|
.setValue(CameraProperties.POSITION, Triple.of(posX, posY, posZ))
|
|
.setValue(CameraProperties.ROTATION, Triple.of(yaw, pitch, roll))
|
|
.done();
|
|
change.apply(timeline);
|
|
return change;
|
|
}
|
|
|
|
public void removePositionKeyframe(long time) {
|
|
LOGGER.debug("Removing position keyframe at {}", time);
|
|
|
|
Path path = positionPath;
|
|
Keyframe keyframe = path.getKeyframe(time);
|
|
|
|
Preconditions.checkState(keyframe != null, "No keyframe at that time");
|
|
|
|
Change change = create(path, keyframe);
|
|
change.apply(timeline);
|
|
|
|
// Update interpolators for spectator keyframes
|
|
// while this is overkill, it is far simpler than updating differently for every possible case
|
|
change = CombinedChange.createFromApplied(change, updateInterpolators());
|
|
|
|
Change specPosUpdate = updateSpectatorPositions();
|
|
specPosUpdate.apply(timeline);
|
|
change = CombinedChange.createFromApplied(change, specPosUpdate);
|
|
|
|
timeline.pushChange(change);
|
|
}
|
|
|
|
public void addTimeKeyframe(long time, int replayTime) {
|
|
LOGGER.debug("Adding time keyframe at {} time {}", time, replayTime);
|
|
|
|
Path path = timePath;
|
|
|
|
Preconditions.checkState(path.getKeyframe(time) == null, "Keyframe already exists");
|
|
|
|
Change change = AddKeyframe.create(path, time);
|
|
change.apply(timeline);
|
|
Keyframe keyframe = path.getKeyframe(time);
|
|
|
|
UpdateKeyframeProperties updateChange = UpdateKeyframeProperties.create(path, keyframe)
|
|
.setValue(TimestampProperty.PROPERTY, replayTime)
|
|
.done();
|
|
updateChange.apply(timeline);
|
|
change = CombinedChange.createFromApplied(change, updateChange);
|
|
|
|
// If this new keyframe formed the first segment of the path
|
|
if (path.getSegments().size() == 1) {
|
|
// then create an initial interpolator
|
|
PathSegment segment = path.getSegments().iterator().next();
|
|
Interpolator interpolator = new LinearInterpolator();
|
|
interpolator.registerProperty(TimestampProperty.PROPERTY);
|
|
SetInterpolator setInterpolator = SetInterpolator.create(segment, interpolator);
|
|
setInterpolator.apply(timeline);
|
|
change = CombinedChange.createFromApplied(change, setInterpolator);
|
|
}
|
|
|
|
Change specPosUpdate = updateSpectatorPositions();
|
|
specPosUpdate.apply(timeline);
|
|
change = CombinedChange.createFromApplied(change, specPosUpdate);
|
|
|
|
timeline.pushChange(change);
|
|
}
|
|
|
|
public Change updateTimeKeyframe(long time, int replayTime) {
|
|
LOGGER.debug("Updating time keyframe at {} to time {}", time, replayTime);
|
|
|
|
Keyframe keyframe = timePath.getKeyframe(time);
|
|
|
|
Preconditions.checkState(keyframe != null, "Keyframe does not exists");
|
|
|
|
Change change = UpdateKeyframeProperties.create(timePath, keyframe)
|
|
.setValue(TimestampProperty.PROPERTY, replayTime)
|
|
.done();
|
|
change.apply(timeline);
|
|
return change;
|
|
}
|
|
|
|
public void removeTimeKeyframe(long time) {
|
|
LOGGER.debug("Removing time keyframe at {}", time);
|
|
|
|
Path path = timePath;
|
|
Keyframe keyframe = path.getKeyframe(time);
|
|
|
|
Preconditions.checkState(keyframe != null, "No keyframe at that time");
|
|
|
|
Change change = create(path, keyframe);
|
|
change.apply(timeline);
|
|
|
|
Change specPosUpdate = updateSpectatorPositions();
|
|
specPosUpdate.apply(timeline);
|
|
change = CombinedChange.createFromApplied(change, specPosUpdate);
|
|
|
|
timeline.pushChange(change);
|
|
}
|
|
|
|
public Change setInterpolatorToDefault(long time) {
|
|
LOGGER.debug("Setting interpolator of position keyframe at {} to the default", time);
|
|
|
|
Keyframe keyframe = positionPath.getKeyframe(time);
|
|
|
|
Preconditions.checkState(keyframe != null, "Keyframe does not exists");
|
|
|
|
Change change = UpdateKeyframeProperties.create(positionPath, keyframe)
|
|
.removeProperty(ExplicitInterpolationProperty.PROPERTY)
|
|
.done();
|
|
change.apply(timeline);
|
|
return CombinedChange.createFromApplied(change, updateInterpolators());
|
|
}
|
|
|
|
public Change setInterpolator(long time, Interpolator interpolator) {
|
|
LOGGER.debug("Setting interpolator of position keyframe at {} to {}", time, interpolator);
|
|
|
|
Keyframe keyframe = positionPath.getKeyframe(time);
|
|
Preconditions.checkState(keyframe != null, "Keyframe does not exists");
|
|
PathSegment segment = positionPath.getSegments().stream().filter(s -> s.getStartKeyframe() == keyframe)
|
|
.findFirst().orElseThrow(() -> new IllegalStateException("Keyframe has no following segment."));
|
|
|
|
registerPositionInterpolatorProperties(interpolator);
|
|
|
|
Change change = CombinedChange.create(
|
|
UpdateKeyframeProperties.create(positionPath, keyframe)
|
|
.setValue(ExplicitInterpolationProperty.PROPERTY, ObjectUtils.NULL)
|
|
.done(),
|
|
SetInterpolator.create(segment, interpolator)
|
|
);
|
|
change.apply(timeline);
|
|
return CombinedChange.createFromApplied(change, updateInterpolators());
|
|
}
|
|
|
|
public Change moveKeyframe(SPPath spPath, long oldTime, long newTime) {
|
|
LOGGER.debug("Moving keyframe on {} from {} to {}", spPath, oldTime, newTime);
|
|
Path path = getPath(spPath);
|
|
Keyframe keyframe = path.getKeyframe(oldTime);
|
|
|
|
Preconditions.checkState(keyframe != null, "No keyframe at specified time");
|
|
|
|
// Interpolator of the first segment might be required later if it is the only one
|
|
Optional<Interpolator> firstInterpolator =
|
|
path.getSegments().stream().findFirst().map(PathSegment::getInterpolator);
|
|
|
|
// The interpolator that will be lost once we remove the old keyframe and has to be restored afterwards
|
|
Optional<Interpolator> lostInterpolator = path.getSegments().stream().filter(s -> {
|
|
// If this is the last keyframe,
|
|
if (Iterables.getLast(path.getKeyframes()) == keyframe) {
|
|
return s.getEndKeyframe() == keyframe; // the previous interpolator will be lost
|
|
} else { // otherwise
|
|
return s.getStartKeyframe() == keyframe; // the following interpolator will be lost
|
|
}
|
|
}).findFirst().map(PathSegment::getInterpolator);
|
|
|
|
// First remove the old keyframe
|
|
Change removeChange = create(path, keyframe);
|
|
removeChange.apply(timeline);
|
|
|
|
// and add a new one at the correct time
|
|
Change addChange = AddKeyframe.create(path, newTime);
|
|
addChange.apply(timeline);
|
|
|
|
// Then copy over all properties
|
|
UpdateKeyframeProperties.Builder builder = UpdateKeyframeProperties.create(path, path.getKeyframe(newTime));
|
|
for (Property property : keyframe.getProperties()) {
|
|
copyProperty(property, keyframe, builder);
|
|
}
|
|
Change propertyChange = builder.done();
|
|
propertyChange.apply(timeline);
|
|
|
|
|
|
Change restoreInterpolatorChange;
|
|
Keyframe newKf = path.getKeyframe(newTime);
|
|
if (Iterables.getLast(path.getKeyframes()) != newKf) { // Unless this is the last keyframe
|
|
// the interpolator of the following segment has been lost and needs to be restored
|
|
restoreInterpolatorChange = lostInterpolator.<Change>flatMap(interpolator ->
|
|
path.getSegments().stream().filter(s -> s.getStartKeyframe() == newKf).findFirst().map(segment ->
|
|
SetInterpolator.create(segment, interpolator)
|
|
)
|
|
).orElseGet(CombinedChange::create);
|
|
} else { // If it is the last keyframe however,
|
|
// the interpolator of the previous segment has been lost and needs to be restored
|
|
restoreInterpolatorChange = path.getSegments().stream().filter(s -> s.getEndKeyframe() == newKf)
|
|
.findFirst().flatMap(segment -> lostInterpolator.map(interpolator -> {
|
|
// additionally, if the interpolation of this keyframe was set to explicit, that property
|
|
// has to be transferred to the start keyframe of the new segment
|
|
if (newKf.getValue(ExplicitInterpolationProperty.PROPERTY).isPresent()) {
|
|
return CombinedChange.create(
|
|
SetInterpolator.create(segment, interpolator),
|
|
UpdateKeyframeProperties.create(path, segment.getStartKeyframe())
|
|
.setValue(ExplicitInterpolationProperty.PROPERTY, ObjectUtils.NULL).done()
|
|
);
|
|
} else {
|
|
return SetInterpolator.create(segment, interpolator);
|
|
}
|
|
})).orElseGet(CombinedChange::create);
|
|
}
|
|
restoreInterpolatorChange.apply(timeline);
|
|
|
|
// Finally update the interpolators
|
|
Change interpolatorUpdateChange;
|
|
if (spPath == SPPath.POSITION) {
|
|
// Position / Spectator keyframes need special handling
|
|
interpolatorUpdateChange = updateInterpolators();
|
|
} else {
|
|
// Time keyframes only need updating when only one segment of them exists
|
|
if (path.getSegments().size() == 1) {
|
|
assert firstInterpolator.isPresent() : "One segment should have existed before as well";
|
|
interpolatorUpdateChange = SetInterpolator.create(path.getSegments().iterator().next(), firstInterpolator.get());
|
|
} else {
|
|
interpolatorUpdateChange = CombinedChange.create(); // Noop change
|
|
}
|
|
interpolatorUpdateChange.apply(timeline);
|
|
}
|
|
|
|
// and update spectator positions
|
|
Change spectatorChange = updateSpectatorPositions();
|
|
spectatorChange.apply(timeline);
|
|
|
|
return CombinedChange.createFromApplied(removeChange, addChange, propertyChange,
|
|
restoreInterpolatorChange, interpolatorUpdateChange, spectatorChange);
|
|
}
|
|
|
|
// Helper method because generics cannot be defined on blocks
|
|
private <T> void copyProperty(Property<T> property, Keyframe from, UpdateKeyframeProperties.Builder to) {
|
|
from.getValue(property).ifPresent(value -> to.setValue(property, value));
|
|
}
|
|
|
|
private Change updateInterpolators() {
|
|
Collection<PathSegment> pathSegments = positionPath.getSegments();
|
|
// Contains updated pathsegment-interpolator mappings, may be changed multiple times over the course of this method
|
|
Map<PathSegment, Interpolator> updates = new HashMap<>();
|
|
|
|
// First, setup spectator interpolators
|
|
Interpolator interpolator = null;
|
|
// Iterate through all segments
|
|
for (PathSegment segment : pathSegments) {
|
|
if (isSpectatorSegment(segment)) {
|
|
// If the last segment was a spectator segment, then use its interpolator for this segment as well
|
|
if (interpolator == null) {
|
|
// otherwise create a new interpolator
|
|
interpolator = new LinearInterpolator();
|
|
interpolator.registerProperty(SpectatorProperty.PROPERTY);
|
|
}
|
|
// Now that we have an interpolator, set it for the current segment
|
|
updates.put(segment, interpolator);
|
|
} else {
|
|
// Not a spectator segment, make sure we don't re-use the last interpolator (it would not be continuous)
|
|
interpolator = null;
|
|
}
|
|
}
|
|
|
|
// Then, if required, replace old default interpolators with new ones
|
|
pathSegments.stream()
|
|
// Ignore explicitly set segments
|
|
.filter(s -> !s.getStartKeyframe().getValue(ExplicitInterpolationProperty.PROPERTY).isPresent())
|
|
// Ignore spectator segments
|
|
.filter(s -> !isSpectatorSegment(s))
|
|
// Ignore already correct segments, should ignore all if default hasn't changed
|
|
.filter(s -> !s.getInterpolator().getClass().equals(defaultInterpolatorType.getInterpolatorClass()))
|
|
// Finally, set the interpolators
|
|
// This will create one interpolator per segment, they will be merged later
|
|
.forEach(segment -> updates.put(segment, createDefaultInterpolator()));
|
|
|
|
// All interpolators should now be set appropriately but may still be fragmented.
|
|
// Cleaning that up is a two step process
|
|
|
|
// Firstly, all interpolators that are not continuous need to be split up
|
|
Interpolator lastInterpolator = null;
|
|
Set<Interpolator> used = Collections.newSetFromMap(new IdentityHashMap<>());
|
|
for (PathSegment segment : pathSegments) {
|
|
if (isSpectatorSegment(segment)) {
|
|
lastInterpolator = null;
|
|
continue; // Chain broken by spectator segment
|
|
}
|
|
|
|
Interpolator currentInterpolator = updates.getOrDefault(segment, segment.getInterpolator());
|
|
if (lastInterpolator == currentInterpolator) {
|
|
continue; // All fine, interpolator is still continuous
|
|
}
|
|
|
|
// New interpolator, make sure it hasn't been used before
|
|
if (!used.add(interpolator)) {
|
|
// It has been used before, we need to clone it and use the clone instead
|
|
// This will create a new interpolator for each segment which will be merged later
|
|
currentInterpolator = cloneInterpolator(currentInterpolator);
|
|
updates.put(segment, currentInterpolator);
|
|
}
|
|
lastInterpolator = currentInterpolator;
|
|
}
|
|
|
|
// Secondly, all neighbouring interpolators that are equal need to be merged
|
|
lastInterpolator = null;
|
|
String lastInterpolatorSerialized = null;
|
|
for (PathSegment segment : pathSegments) {
|
|
if (isSpectatorSegment(segment)) {
|
|
lastInterpolator = null;
|
|
lastInterpolatorSerialized = null;
|
|
continue; // Spectator segments are continuous by construction
|
|
}
|
|
Interpolator currentInterpolator = updates.getOrDefault(segment, segment.getInterpolator());
|
|
String serialized = serializeInterpolator(currentInterpolator);
|
|
if (lastInterpolator != currentInterpolator && serialized.equals(lastInterpolatorSerialized)) {
|
|
// This interpolator is equal to the last one but not the same, needs merging
|
|
updates.put(segment, lastInterpolator);
|
|
continue;
|
|
}
|
|
// New interpolator, not related to the previous one, merged by definition
|
|
lastInterpolator = currentInterpolator;
|
|
lastInterpolatorSerialized = serialized;
|
|
}
|
|
|
|
Change change = CombinedChange.create(updates.entrySet().stream()
|
|
.map(e -> SetInterpolator.create(e.getKey(), e.getValue())).toArray(Change[]::new));
|
|
change.apply(timeline);
|
|
return change;
|
|
}
|
|
|
|
private boolean isSpectatorSegment(PathSegment segment) {
|
|
return segment.getStartKeyframe().getValue(SpectatorProperty.PROPERTY).isPresent()
|
|
&& segment.getEndKeyframe().getValue(SpectatorProperty.PROPERTY).isPresent();
|
|
}
|
|
|
|
private Change updateSpectatorPositions() {
|
|
List<Change> changes = new ArrayList<>();
|
|
timePath.updateAll();
|
|
for (Keyframe keyframe : positionPath.getKeyframes()) {
|
|
Optional<Integer> spectator = keyframe.getValue(SpectatorProperty.PROPERTY);
|
|
if (spectator.isPresent()) {
|
|
Optional<Integer> time = timePath.getValue(TimestampProperty.PROPERTY, keyframe.getTime());
|
|
if (!time.isPresent()) {
|
|
continue; // No time keyframes set at this video time, cannot determine replay time
|
|
}
|
|
Location expected = entityTracker.getEntityPositionAtTimestamp(spectator.get(), time.get());
|
|
if (expected == null) {
|
|
continue; // We don't have any data on this entity for some reason
|
|
}
|
|
Triple<Double, Double, Double> pos = keyframe.getValue(CameraProperties.POSITION).orElse(Triple.of(0D, 0D, 0D));
|
|
Triple<Float, Float, Float> rot = keyframe.getValue(CameraProperties.ROTATION).orElse(Triple.of(0F, 0F, 0F));
|
|
Location actual = new Location(pos.getLeft(), pos.getMiddle(), pos.getRight(), rot.getLeft(), rot.getRight());
|
|
if (!expected.equals(actual)) {
|
|
changes.add(UpdateKeyframeProperties.create(positionPath, keyframe)
|
|
.setValue(CameraProperties.POSITION, Triple.of(expected.getX(), expected.getY(), expected.getZ()))
|
|
.setValue(CameraProperties.ROTATION, Triple.of(expected.getYaw(), expected.getPitch(), 0f)).done()
|
|
);
|
|
}
|
|
}
|
|
}
|
|
return CombinedChange.create(changes.toArray(new Change[changes.size()]));
|
|
}
|
|
|
|
private Interpolator createDefaultInterpolator() {
|
|
return registerPositionInterpolatorProperties(defaultInterpolatorType.newInstance());
|
|
}
|
|
|
|
private Interpolator registerPositionInterpolatorProperties(Interpolator interpolator) {
|
|
interpolator.registerProperty(CameraProperties.POSITION);
|
|
interpolator.registerProperty(CameraProperties.ROTATION);
|
|
return interpolator;
|
|
}
|
|
|
|
@Override
|
|
public Timeline createTimeline() {
|
|
return createTimelineStatic();
|
|
}
|
|
|
|
private static Timeline createInitialTimeline() {
|
|
Timeline timeline = createTimelineStatic();
|
|
timeline.createPath();
|
|
timeline.createPath();
|
|
return timeline;
|
|
}
|
|
|
|
private static Timeline createTimelineStatic() {
|
|
Timeline timeline = new TimelineImpl();
|
|
|
|
timeline.registerProperty(TimestampProperty.PROPERTY);
|
|
timeline.registerProperty(CameraProperties.POSITION);
|
|
timeline.registerProperty(CameraProperties.ROTATION);
|
|
timeline.registerProperty(SpectatorProperty.PROPERTY);
|
|
timeline.registerProperty(ExplicitInterpolationProperty.PROPERTY);
|
|
|
|
return timeline;
|
|
}
|
|
|
|
@Override
|
|
public void serializeInterpolator(JsonWriter writer, Interpolator interpolator) throws IOException {
|
|
if (interpolator instanceof LinearInterpolator) {
|
|
writer.value("linear");
|
|
} else if (interpolator instanceof CubicSplineInterpolator) {
|
|
writer.value("cubic-spline");
|
|
} else if (interpolator instanceof CatmullRomSplineInterpolator) {
|
|
writer.beginObject();
|
|
writer.name("type").value("catmull-rom-spline");
|
|
writer.name("alpha").value(((CatmullRomSplineInterpolator) interpolator).getAlpha());
|
|
writer.endObject();
|
|
} else {
|
|
throw new IOException("Unknown interpolator type: " + interpolator);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public Interpolator deserializeInterpolator(JsonReader reader) throws IOException {
|
|
String type;
|
|
JsonObject args;
|
|
switch (reader.peek()) {
|
|
case STRING:
|
|
type = reader.nextString();
|
|
args = null;
|
|
break;
|
|
case BEGIN_OBJECT:
|
|
args = new JsonParser().parse(reader).getAsJsonObject();
|
|
type = args.get("type").getAsString();
|
|
break;
|
|
default:
|
|
throw new IOException("Unexpected token: " + reader.peek());
|
|
}
|
|
switch (type) {
|
|
case "linear":
|
|
return new LinearInterpolator();
|
|
case "cubic-spline":
|
|
return new CubicSplineInterpolator();
|
|
case "catmull-rom-spline":
|
|
if (args == null || !args.has("alpha")) {
|
|
throw new IOException("Missing alpha value for catmull-rom-spline.");
|
|
}
|
|
return new CatmullRomSplineInterpolator(args.get("alpha").getAsDouble());
|
|
default:
|
|
throw new IOException("Unknown interpolation type: " + type);
|
|
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clones an interpolator by de- and re-serializing it.
|
|
* @param interpolator The interpolator to clone
|
|
* @return The cloned interpolator
|
|
*/
|
|
private Interpolator cloneInterpolator(Interpolator interpolator) {
|
|
Interpolator cloned = deserializeInterpolator(serializeInterpolator(interpolator));
|
|
interpolator.getKeyframeProperties().forEach(cloned::registerProperty);
|
|
return cloned;
|
|
}
|
|
|
|
/**
|
|
* Serializes the specific interpolator to String.
|
|
* Does <b>not</b> serialize the registered keyframe properties.
|
|
* @param interpolator The interpolator to serialize.
|
|
* @return The serialized interpolator
|
|
*/
|
|
private String serializeInterpolator(Interpolator interpolator) {
|
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
|
|
|
JsonWriter jsonWriter = new JsonWriter(new PrintWriter(baos));
|
|
try {
|
|
jsonWriter.beginArray();
|
|
serializeInterpolator(jsonWriter, interpolator);
|
|
jsonWriter.endArray();
|
|
jsonWriter.flush();
|
|
} catch (IOException e) {
|
|
CrashReport crash = CrashReport.makeCrashReport(e, "Serializing interpolator");
|
|
CrashReportCategory category = crash.makeCategory("Serializing interpolator");
|
|
category.setDetail("Interpolator", interpolator::toString);
|
|
throw new ReportedException(crash);
|
|
}
|
|
|
|
return baos.toString();
|
|
}
|
|
|
|
private Interpolator deserializeInterpolator(String json) {
|
|
JsonReader jsonReader = new JsonReader(new StringReader(json));
|
|
try {
|
|
jsonReader.beginArray();
|
|
return deserializeInterpolator(jsonReader);
|
|
} catch (IOException e) {
|
|
CrashReport crash = CrashReport.makeCrashReport(e, "De-serializing interpolator");
|
|
CrashReportCategory category = crash.makeCategory("De-serializing interpolator");
|
|
category.addCrashSection("Interpolator", json);
|
|
throw new ReportedException(crash);
|
|
}
|
|
}
|
|
}
|