Merge branch '1.8' into 1.8.9
1ffeb01Update jGuic122d30Merge branch '1.8-path-segments' into 1.827fd25aFix crash when server responds with garbage to language query (fixes #51)db6b0ffFix / by 0 when two spectator keyframes are closer than 50ms (fixes #60)d61d0acFix hand of invisible spectated player being visible (fixes #59)f7460f9Re-enable time path after path playback (fixes #55)bdbffd5Add Catmull-Rom spline interpolator to simplepathing5cbfd28Generate interpolator settings from InterpolatorType enum6095aefFix loading and storing of default interpolator with settings2c64030Refactor simple pathing timeline logic into dedicated class with unit tests Fix logic for user-set interpolatorscd16211Register new Property to imply that the path segment following a keyframe has a user-set interpolator instead of the default Modified Keyframe GUI to allow for custom Path interpolator
This commit is contained in:
@@ -82,7 +82,7 @@ public class LocalizationExtra implements Extra {
|
||||
availableLanguages.put(langcode, prop);
|
||||
return true;
|
||||
} catch (ApiException e) {
|
||||
if (e.getError().getId() != 16) { // This language has not been translated
|
||||
if (e.getError() == null || e.getError().getId() != 16) { // This language has not been translated
|
||||
e.printStackTrace();
|
||||
}
|
||||
} catch(ConnectException ce) {
|
||||
|
||||
@@ -11,7 +11,10 @@ import com.replaymod.replay.events.ReplayCloseEvent;
|
||||
import com.replaymod.replay.events.ReplayOpenEvent;
|
||||
import net.minecraft.client.renderer.entity.RenderManager;
|
||||
import net.minecraft.client.renderer.entity.RenderPlayer;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.entity.player.EntityPlayer;
|
||||
import net.minecraftforge.client.event.RenderHandEvent;
|
||||
import net.minecraftforge.common.MinecraftForge;
|
||||
import net.minecraftforge.fml.common.FMLCommonHandler;
|
||||
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
|
||||
import org.lwjgl.input.Keyboard;
|
||||
@@ -56,6 +59,7 @@ public class PlayerOverview implements Extra {
|
||||
});
|
||||
|
||||
FMLCommonHandler.instance().bus().register(this);
|
||||
MinecraftForge.EVENT_BUS.register(this);
|
||||
|
||||
RenderManager renderManager = mod.getMinecraft().getRenderManager();
|
||||
@SuppressWarnings("unchecked")
|
||||
@@ -92,6 +96,14 @@ public class PlayerOverview implements Extra {
|
||||
hiddenPlayers.clear();
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public void oRenderHand(RenderHandEvent event) {
|
||||
Entity view = module.getCore().getMinecraft().getRenderViewEntity();
|
||||
if (view != null && isHidden(view.getUniqueID())) {
|
||||
event.setCanceled(true);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isSavingEnabled() {
|
||||
return savingEnabled;
|
||||
}
|
||||
|
||||
@@ -252,6 +252,15 @@ public class CameraEntity extends EntityPlayerSP {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInvisible() {
|
||||
Entity view = mc.getRenderViewEntity();
|
||||
if (view != this) {
|
||||
return view.isInvisible();
|
||||
}
|
||||
return super.isInvisible();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourceLocation getLocationSkin() {
|
||||
Entity view = mc.getRenderViewEntity();
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.replaymod.simplepathing;
|
||||
|
||||
import com.replaymod.replaystudio.pathing.interpolation.CatmullRomSplineInterpolator;
|
||||
import com.replaymod.replaystudio.pathing.interpolation.CubicSplineInterpolator;
|
||||
import com.replaymod.replaystudio.pathing.interpolation.Interpolator;
|
||||
import com.replaymod.replaystudio.pathing.interpolation.LinearInterpolator;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@AllArgsConstructor
|
||||
public enum InterpolatorType {
|
||||
DEFAULT("default", null, null),
|
||||
CATMULL_ROM("catmullrom", CatmullRomSplineInterpolator.class, () -> new CatmullRomSplineInterpolator(0.5)),
|
||||
CUBIC("cubic", CubicSplineInterpolator.class, CubicSplineInterpolator::new),
|
||||
LINEAR("linear", LinearInterpolator.class, LinearInterpolator::new);
|
||||
|
||||
@Getter
|
||||
private String localizationKey;
|
||||
|
||||
@Getter
|
||||
private Class<? extends Interpolator> interpolatorClass;
|
||||
|
||||
private Supplier<Interpolator> interpolatorConstructor;
|
||||
|
||||
public String getI18nName() {
|
||||
return String.format("replaymod.gui.editkeyframe.interpolator.%1$s.name", localizationKey);
|
||||
}
|
||||
|
||||
public String getI18nDescription() {
|
||||
return String.format("replaymod.gui.editkeyframe.interpolator.%1$s.desc", localizationKey);
|
||||
}
|
||||
|
||||
public static InterpolatorType fromString(String string) {
|
||||
for (InterpolatorType t : values()) {
|
||||
if (t.getI18nName().equals(string)) return t;
|
||||
}
|
||||
return CATMULL_ROM; //the default
|
||||
}
|
||||
|
||||
public static InterpolatorType fromClass(Class<? extends Interpolator> cls) {
|
||||
for (InterpolatorType type : values()) {
|
||||
if (cls.equals(type.getInterpolatorClass())) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
return DEFAULT;
|
||||
}
|
||||
|
||||
public Interpolator newInstance() {
|
||||
return interpolatorConstructor.get();
|
||||
}
|
||||
}
|
||||
@@ -1,36 +1,25 @@
|
||||
package com.replaymod.simplepathing;
|
||||
|
||||
import com.google.gson.stream.JsonReader;
|
||||
import com.google.gson.stream.JsonWriter;
|
||||
import com.replaymod.core.ReplayMod;
|
||||
import com.replaymod.core.events.SettingsChangedEvent;
|
||||
import com.replaymod.pathing.properties.CameraProperties;
|
||||
import com.replaymod.pathing.properties.SpectatorProperty;
|
||||
import com.replaymod.pathing.properties.TimestampProperty;
|
||||
import com.replaymod.replay.events.ReplayCloseEvent;
|
||||
import com.replaymod.replay.events.ReplayOpenEvent;
|
||||
import com.replaymod.replaystudio.pathing.PathingRegistry;
|
||||
import com.replaymod.replaystudio.pathing.impl.TimelineImpl;
|
||||
import com.replaymod.replaystudio.pathing.interpolation.CubicSplineInterpolator;
|
||||
import com.replaymod.replaystudio.pathing.interpolation.Interpolator;
|
||||
import com.replaymod.replaystudio.pathing.interpolation.LinearInterpolator;
|
||||
import com.replaymod.replaystudio.pathing.path.Keyframe;
|
||||
import com.replaymod.replaystudio.pathing.path.Timeline;
|
||||
import com.replaymod.simplepathing.SPTimeline.SPPath;
|
||||
import com.replaymod.simplepathing.gui.GuiPathing;
|
||||
import com.replaymod.simplepathing.preview.PathPreview;
|
||||
import lombok.Getter;
|
||||
import net.minecraftforge.fml.common.FMLCommonHandler;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
import net.minecraftforge.fml.common.event.FMLPreInitializationEvent;
|
||||
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@Mod(modid = ReplayModSimplePathing.MOD_ID,
|
||||
version = "@MOD_VERSION@",
|
||||
acceptedMinecraftVersions = "@MC_VERSION@",
|
||||
useMetadata = true)
|
||||
public class ReplayModSimplePathing implements PathingRegistry {
|
||||
public class ReplayModSimplePathing {
|
||||
public static final String MOD_ID = "replaymod-simplepathing";
|
||||
|
||||
private ReplayMod core;
|
||||
@@ -54,84 +43,60 @@ public class ReplayModSimplePathing implements PathingRegistry {
|
||||
|
||||
@SubscribeEvent
|
||||
public void postReplayOpen(ReplayOpenEvent.Post event) {
|
||||
currentTimeline = new SPTimeline();
|
||||
guiPathing = new GuiPathing(core, this, event.getReplayHandler());
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public void onReplayClose(ReplayCloseEvent.Post event) {
|
||||
currentTimeline = null;
|
||||
guiPathing = null;
|
||||
currentTimeline = createTimeline();
|
||||
currentTimeline.createPath();
|
||||
currentTimeline.createPath();
|
||||
selectedKeyframe = null;
|
||||
selectedPath = null;
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public void onSettingsChanged(SettingsChangedEvent event) {
|
||||
if (event.getKey() == Setting.LINEAR_INTERPOLATION) {
|
||||
if (event.getKey() == Setting.DEFAULT_INTERPOLATION) {
|
||||
if (currentTimeline != null && guiPathing != null) {
|
||||
currentTimeline.applyChange(guiPathing.updateInterpolators());
|
||||
updateDefaultInterpolatorType();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Timeline currentTimeline = createTimeline(); { currentTimeline.createPath(); currentTimeline.createPath(); }
|
||||
private Keyframe selectedKeyframe;
|
||||
private SPTimeline currentTimeline;
|
||||
|
||||
public Keyframe getSelectedKeyframe() {
|
||||
return selectedKeyframe;
|
||||
@Getter
|
||||
private SPPath selectedPath;
|
||||
@Getter
|
||||
private long selectedTime;
|
||||
|
||||
public boolean isSelected(Keyframe keyframe) {
|
||||
return selectedPath != null && currentTimeline.getKeyframe(selectedPath, selectedTime) == keyframe;
|
||||
}
|
||||
|
||||
public void setSelectedKeyframe(Keyframe selected) {
|
||||
this.selectedKeyframe = selected;
|
||||
public void setSelected(SPPath path, long time) {
|
||||
selectedPath = path;
|
||||
selectedTime = time;
|
||||
}
|
||||
|
||||
public void setCurrentTimeline(Timeline currentTimeline) {
|
||||
if (this.currentTimeline != currentTimeline) {
|
||||
selectedKeyframe = null;
|
||||
}
|
||||
this.currentTimeline = currentTimeline;
|
||||
public void setCurrentTimeline(SPTimeline newTimeline) {
|
||||
selectedPath = null;
|
||||
currentTimeline = newTimeline;
|
||||
}
|
||||
|
||||
public Timeline getCurrentTimeline() {
|
||||
public void clearCurrentTimeline() {
|
||||
setCurrentTimeline(new SPTimeline());
|
||||
updateDefaultInterpolatorType();
|
||||
}
|
||||
|
||||
public SPTimeline getCurrentTimeline() {
|
||||
return currentTimeline;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Timeline createTimeline() {
|
||||
Timeline timeline = new TimelineImpl();
|
||||
|
||||
timeline.registerProperty(TimestampProperty.PROPERTY);
|
||||
timeline.registerProperty(CameraProperties.POSITION);
|
||||
timeline.registerProperty(CameraProperties.ROTATION);
|
||||
timeline.registerProperty(SpectatorProperty.PROPERTY);
|
||||
|
||||
return timeline;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serializeInterpolator(JsonWriter writer, Interpolator interpolator) throws IOException {
|
||||
if (interpolator instanceof LinearInterpolator) {
|
||||
writer.value("linear");
|
||||
} else if (interpolator instanceof CubicSplineInterpolator) {
|
||||
writer.value("cubic-spline");
|
||||
} else {
|
||||
throw new IOException("Unknown interpolator type: " + interpolator);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Interpolator deserializeInterpolator(JsonReader reader) throws IOException {
|
||||
String type = reader.nextString();
|
||||
switch (type) {
|
||||
case "linear":
|
||||
return new LinearInterpolator();
|
||||
case "cubic-spline":
|
||||
return new CubicSplineInterpolator();
|
||||
default:
|
||||
throw new IOException("Unknown interpolation type: " + type);
|
||||
|
||||
}
|
||||
private void updateDefaultInterpolatorType() {
|
||||
InterpolatorType newDefaultType =
|
||||
InterpolatorType.fromString(core.getSettingsRegistry().get(Setting.DEFAULT_INTERPOLATION));
|
||||
currentTimeline.setDefaultInterpolatorType(newDefaultType);
|
||||
}
|
||||
|
||||
public ReplayMod getCore() {
|
||||
|
||||
669
src/main/java/com/replaymod/simplepathing/SPTimeline.java
Normal file
669
src/main/java/com/replaymod/simplepathing/SPTimeline.java
Normal file
@@ -0,0 +1,669 @@
|
||||
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 = InterpolatorType.fromString("invalid string returns default");
|
||||
|
||||
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 of the previous segment
|
||||
Optional<Interpolator> interpolatorBefore =
|
||||
path.getSegments().stream().filter(s -> s.getEndKeyframe() == keyframe).findFirst().map(PathSegment::getInterpolator);
|
||||
|
||||
// The interpolator of the following segment
|
||||
Optional<Interpolator> interpolatorAfter =
|
||||
path.getSegments().stream().filter(s -> s.getStartKeyframe() == keyframe).findFirst().map(PathSegment::getInterpolator);
|
||||
|
||||
// First remove the old keyframe
|
||||
Change removeChange = create(path, keyframe);
|
||||
removeChange.apply(timeline);
|
||||
|
||||
// and add a new one at the correct time
|
||||
Change addChange = AddKeyframe.create(path, newTime);
|
||||
addChange.apply(timeline);
|
||||
|
||||
// Then copy over all properties
|
||||
UpdateKeyframeProperties.Builder builder = UpdateKeyframeProperties.create(path, path.getKeyframe(newTime));
|
||||
for (Property property : keyframe.getProperties()) {
|
||||
copyProperty(property, keyframe, builder);
|
||||
}
|
||||
Change propertyChange = builder.done();
|
||||
propertyChange.apply(timeline);
|
||||
|
||||
|
||||
Change restoreInterpolatorChange;
|
||||
Keyframe newKf = path.getKeyframe(newTime);
|
||||
if (Iterables.getLast(path.getKeyframes()) != newKf) { // Unless this is the last keyframe
|
||||
// the interpolator of the following segment has been lost and needs to be restored
|
||||
restoreInterpolatorChange = interpolatorAfter.<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 = interpolatorBefore.<Change>flatMap(interpolator ->
|
||||
path.getSegments().stream().filter(s -> s.getEndKeyframe() == newKf).findFirst().map(segment ->
|
||||
SetInterpolator.create(segment, interpolator)
|
||||
)
|
||||
).orElseGet(CombinedChange::create);
|
||||
}
|
||||
restoreInterpolatorChange.apply(timeline);
|
||||
|
||||
// Finally update the interpolators
|
||||
Change interpolatorUpdateChange;
|
||||
if (spPath == SPPath.POSITION) {
|
||||
// Position / Spectator keyframes need special handling
|
||||
interpolatorUpdateChange = updateInterpolators();
|
||||
} else {
|
||||
// Time keyframes only need updating when only one segment of them exists
|
||||
if (path.getSegments().size() == 1) {
|
||||
assert firstInterpolator.isPresent() : "One segment should have existed before as well";
|
||||
interpolatorUpdateChange = SetInterpolator.create(path.getSegments().iterator().next(), firstInterpolator.get());
|
||||
} else {
|
||||
interpolatorUpdateChange = CombinedChange.create(); // Noop change
|
||||
}
|
||||
interpolatorUpdateChange.apply(timeline);
|
||||
}
|
||||
|
||||
// and update spectator positions
|
||||
Change spectatorChange = updateSpectatorPositions();
|
||||
spectatorChange.apply(timeline);
|
||||
|
||||
return CombinedChange.createFromApplied(removeChange, addChange, propertyChange,
|
||||
restoreInterpolatorChange, interpolatorUpdateChange, spectatorChange);
|
||||
}
|
||||
|
||||
// Helper method because generics cannot be defined on blocks
|
||||
private <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.addCrashSectionCallable("Interpolator", interpolator::toString);
|
||||
throw new ReportedException(crash);
|
||||
}
|
||||
|
||||
return baos.toString();
|
||||
}
|
||||
|
||||
private Interpolator deserializeInterpolator(String json) {
|
||||
JsonReader jsonReader = new JsonReader(new StringReader(json));
|
||||
try {
|
||||
jsonReader.beginArray();
|
||||
return deserializeInterpolator(jsonReader);
|
||||
} catch (IOException e) {
|
||||
CrashReport crash = CrashReport.makeCrashReport(e, "De-serializing interpolator");
|
||||
CrashReportCategory category = crash.makeCategory("De-serializing interpolator");
|
||||
category.addCrashSection("Interpolator", json);
|
||||
throw new ReportedException(crash);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,25 @@ package com.replaymod.simplepathing;
|
||||
|
||||
import com.replaymod.core.SettingsRegistry;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public final class Setting<T> extends SettingsRegistry.SettingKeys<T> {
|
||||
public static final Setting<Boolean> PATH_PREVIEW = make("pathpreview", "pathpreview", true);
|
||||
public static final Setting<Boolean> LINEAR_INTERPOLATION = make("linear_interpolation", "linearinterpolation", false);
|
||||
public static final SettingsRegistry.MultipleChoiceSettingKeys<String> DEFAULT_INTERPOLATION;
|
||||
|
||||
static {
|
||||
String format = "replaymod.gui.editkeyframe.interpolator.%s.name";
|
||||
DEFAULT_INTERPOLATION = new SettingsRegistry.MultipleChoiceSettingKeys<>(
|
||||
"simplepathing", "interpolator", "replaymod.gui.settings.interpolator",
|
||||
String.format(format, InterpolatorType.fromString("invalid returns default").getLocalizationKey())
|
||||
);
|
||||
DEFAULT_INTERPOLATION.setChoices(
|
||||
Arrays.stream(InterpolatorType.values()).filter(i -> i != InterpolatorType.DEFAULT)
|
||||
.map(i -> String.format(format, i.getLocalizationKey()))
|
||||
.collect(Collectors.toList())
|
||||
);
|
||||
}
|
||||
|
||||
private static <T> Setting<T> make(String key, String displayName, T defaultValue) {
|
||||
return new Setting<>(key, displayName, defaultValue);
|
||||
|
||||
@@ -5,14 +5,27 @@ import com.replaymod.pathing.properties.TimestampProperty;
|
||||
import com.replaymod.replay.ReplayModReplay;
|
||||
import com.replaymod.replaystudio.pathing.change.Change;
|
||||
import com.replaymod.replaystudio.pathing.change.CombinedChange;
|
||||
import com.replaymod.replaystudio.pathing.change.UpdateKeyframeProperties;
|
||||
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.simplepathing.InterpolatorType;
|
||||
import com.replaymod.simplepathing.SPTimeline;
|
||||
import com.replaymod.simplepathing.SPTimeline.SPPath;
|
||||
import com.replaymod.simplepathing.Setting;
|
||||
import com.replaymod.simplepathing.properties.ExplicitInterpolationProperty;
|
||||
import de.johni0702.minecraft.gui.container.AbstractGuiContainer;
|
||||
import de.johni0702.minecraft.gui.container.GuiPanel;
|
||||
import de.johni0702.minecraft.gui.element.GuiButton;
|
||||
import de.johni0702.minecraft.gui.element.GuiLabel;
|
||||
import de.johni0702.minecraft.gui.element.GuiNumberField;
|
||||
import de.johni0702.minecraft.gui.element.GuiTooltip;
|
||||
import de.johni0702.minecraft.gui.element.IGuiClickable;
|
||||
import de.johni0702.minecraft.gui.element.IGuiLabel;
|
||||
import de.johni0702.minecraft.gui.element.advanced.GuiDropdownMenu;
|
||||
import de.johni0702.minecraft.gui.function.Typeable;
|
||||
import de.johni0702.minecraft.gui.layout.GridLayout;
|
||||
import de.johni0702.minecraft.gui.layout.HorizontalLayout;
|
||||
@@ -20,10 +33,16 @@ import de.johni0702.minecraft.gui.layout.VerticalLayout;
|
||||
import de.johni0702.minecraft.gui.popup.AbstractGuiPopup;
|
||||
import de.johni0702.minecraft.gui.utils.Colors;
|
||||
import de.johni0702.minecraft.gui.utils.Consumer;
|
||||
import org.apache.commons.lang3.tuple.Triple;
|
||||
import lombok.Getter;
|
||||
import net.minecraft.client.resources.I18n;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.lwjgl.input.Keyboard;
|
||||
import org.lwjgl.util.ReadablePoint;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static de.johni0702.minecraft.gui.utils.Utils.link;
|
||||
|
||||
public abstract class GuiEditKeyframe<T extends GuiEditKeyframe<T>> extends AbstractGuiPopup<T> implements Typeable {
|
||||
@@ -31,6 +50,11 @@ public abstract class GuiEditKeyframe<T extends GuiEditKeyframe<T>> extends Abst
|
||||
return new GuiNumberField().setPrecision(0).setValidateOnFocusChange(true);
|
||||
}
|
||||
|
||||
protected static final Logger logger = LogManager.getLogger();
|
||||
|
||||
protected final GuiPathing guiPathing;
|
||||
|
||||
protected final long time;
|
||||
protected final Keyframe keyframe;
|
||||
protected final Path path;
|
||||
|
||||
@@ -66,12 +90,13 @@ public abstract class GuiEditKeyframe<T extends GuiEditKeyframe<T>> extends Abst
|
||||
.addElements(new VerticalLayout.Data(0.5, false), title, inputs, timePanel, buttons);
|
||||
}
|
||||
|
||||
public GuiEditKeyframe(GuiPathing gui, Path path, Keyframe keyframe, String type) {
|
||||
public GuiEditKeyframe(GuiPathing gui, SPPath path, long time, String type) {
|
||||
super(ReplayModReplay.instance.getReplayHandler().getOverlay());
|
||||
this.keyframe = keyframe;
|
||||
this.path = path;
|
||||
this.guiPathing = gui;
|
||||
this.time = time;
|
||||
this.path = gui.getMod().getCurrentTimeline().getPath(path);
|
||||
this.keyframe = this.path.getKeyframe(time);
|
||||
|
||||
long time = keyframe.getTime();
|
||||
Consumer<String> updateSaveButtonState = s -> saveButton.setEnabled(canSave());
|
||||
timeMinField.setValue(time / 1000 / 60).onTextChanged(updateSaveButtonState);
|
||||
timeSecField.setValue(time / 1000 % 60).onTextChanged(updateSaveButtonState);
|
||||
@@ -81,10 +106,11 @@ public abstract class GuiEditKeyframe<T extends GuiEditKeyframe<T>> extends Abst
|
||||
saveButton.onClick(() -> {
|
||||
Change change = save();
|
||||
long newTime = (timeMinField.getInteger() * 60 + timeSecField.getInteger()) * 1000 + timeMSecField.getInteger();
|
||||
if (newTime != keyframe.getTime()) {
|
||||
change = CombinedChange.createFromApplied(change, gui.moveKeyframe(path, keyframe, newTime));
|
||||
if (newTime != time) {
|
||||
change = CombinedChange.createFromApplied(change,
|
||||
gui.getMod().getCurrentTimeline().moveKeyframe(path, time, newTime));
|
||||
}
|
||||
path.getTimeline().pushChange(change);
|
||||
gui.getMod().getCurrentTimeline().getTimeline().pushChange(change);
|
||||
close();
|
||||
});
|
||||
}
|
||||
@@ -114,7 +140,7 @@ public abstract class GuiEditKeyframe<T extends GuiEditKeyframe<T>> extends Abst
|
||||
protected abstract Change save();
|
||||
|
||||
public static class Spectator extends GuiEditKeyframe<Spectator> {
|
||||
public Spectator(GuiPathing gui, Path path, Keyframe keyframe) {
|
||||
public Spectator(GuiPathing gui, SPPath path, long keyframe) {
|
||||
super(gui, path, keyframe, "spec");
|
||||
|
||||
link(timeMinField, timeSecField, timeMSecField);
|
||||
@@ -147,10 +173,10 @@ public abstract class GuiEditKeyframe<T extends GuiEditKeyframe<T>> extends Abst
|
||||
timestampMSecField, new GuiLabel().setI18nText("replaymod.gui.milliseconds"));
|
||||
}
|
||||
|
||||
public Time(GuiPathing gui, Path path, Keyframe keyframe) {
|
||||
public Time(GuiPathing gui, SPPath path, long keyframe) {
|
||||
super(gui, path, keyframe, "time");
|
||||
|
||||
keyframe.getValue(TimestampProperty.PROPERTY).ifPresent(time -> {
|
||||
this.keyframe.getValue(TimestampProperty.PROPERTY).ifPresent(time -> {
|
||||
timestampMinField.setValue(time / 1000 / 60);
|
||||
timestampSecField.setValue(time / 1000 % 60);
|
||||
timestampMSecField.setValue(time % 1000);
|
||||
@@ -165,11 +191,7 @@ public abstract class GuiEditKeyframe<T extends GuiEditKeyframe<T>> extends Abst
|
||||
protected Change save() {
|
||||
int time = (timestampMinField.getInteger() * 60 + timestampSecField.getInteger()) * 1000
|
||||
+ timestampMSecField.getInteger();
|
||||
Change change = UpdateKeyframeProperties.create(path, keyframe)
|
||||
.setValue(TimestampProperty.PROPERTY, time)
|
||||
.done();
|
||||
change.apply(path.getTimeline());
|
||||
return change;
|
||||
return guiPathing.getMod().getCurrentTimeline().updateTimeKeyframe(keyframe.getTime(), time);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -187,8 +209,11 @@ public abstract class GuiEditKeyframe<T extends GuiEditKeyframe<T>> extends Abst
|
||||
public final GuiNumberField pitchField = newGuiNumberField().setSize(60, 20).setPrecision(5);
|
||||
public final GuiNumberField rollField = newGuiNumberField().setSize(60, 20).setPrecision(5);
|
||||
|
||||
public final InterpolationPanel interpolationPanel = new InterpolationPanel();
|
||||
|
||||
{
|
||||
inputs.setLayout(new GridLayout().setCellsEqualSize(false).setColumns(4).setSpacingX(3).setSpacingY(5))
|
||||
GuiPanel positionInputs = new GuiPanel()
|
||||
.setLayout(new GridLayout().setCellsEqualSize(false).setColumns(4).setSpacingX(3).setSpacingY(5))
|
||||
.addElements(new GridLayout.Data(1, 0.5),
|
||||
new GuiLabel().setI18nText("replaymod.gui.editkeyframe.xpos"), xField,
|
||||
new GuiLabel().setI18nText("replaymod.gui.editkeyframe.camyaw"), yawField,
|
||||
@@ -196,17 +221,20 @@ public abstract class GuiEditKeyframe<T extends GuiEditKeyframe<T>> extends Abst
|
||||
new GuiLabel().setI18nText("replaymod.gui.editkeyframe.campitch"), pitchField,
|
||||
new GuiLabel().setI18nText("replaymod.gui.editkeyframe.zpos"), zField,
|
||||
new GuiLabel().setI18nText("replaymod.gui.editkeyframe.camroll"), rollField);
|
||||
|
||||
inputs.setLayout(new VerticalLayout().setSpacing(10)).addElements(new VerticalLayout.Data(0.5, false),
|
||||
positionInputs, interpolationPanel);
|
||||
}
|
||||
|
||||
public Position(GuiPathing gui, Path path, Keyframe keyframe) {
|
||||
public Position(GuiPathing gui, SPPath path, long keyframe) {
|
||||
super(gui, path, keyframe, "pos");
|
||||
|
||||
keyframe.getValue(CameraProperties.POSITION).ifPresent(pos -> {
|
||||
this.keyframe.getValue(CameraProperties.POSITION).ifPresent(pos -> {
|
||||
xField.setValue(pos.getLeft());
|
||||
yField.setValue(pos.getMiddle());
|
||||
zField.setValue(pos.getRight());
|
||||
});
|
||||
keyframe.getValue(CameraProperties.ROTATION).ifPresent(rot -> {
|
||||
this.keyframe.getValue(CameraProperties.ROTATION).ifPresent(rot -> {
|
||||
yawField.setValue(rot.getLeft());
|
||||
pitchField.setValue(rot.getMiddle());
|
||||
rollField.setValue(rot.getRight());
|
||||
@@ -219,17 +247,176 @@ public abstract class GuiEditKeyframe<T extends GuiEditKeyframe<T>> extends Abst
|
||||
|
||||
@Override
|
||||
protected Change save() {
|
||||
Change change = UpdateKeyframeProperties.create(path, keyframe)
|
||||
.setValue(CameraProperties.POSITION, Triple.of(xField.getDouble(), yField.getDouble(), zField.getDouble()))
|
||||
.setValue(CameraProperties.ROTATION, Triple.of(yawField.getFloat(), pitchField.getFloat(), rollField.getFloat()))
|
||||
.done();
|
||||
change.apply(path.getTimeline());
|
||||
return change;
|
||||
SPTimeline timeline = guiPathing.getMod().getCurrentTimeline();
|
||||
Change positionChange = timeline.updatePositionKeyframe(time,
|
||||
xField.getDouble(), yField.getDouble(), zField.getDouble(),
|
||||
yawField.getFloat(), pitchField.getFloat(), rollField.getFloat()
|
||||
);
|
||||
Interpolator interpolator = interpolationPanel.getSettingsPanel().createInterpolator();
|
||||
if (interpolationPanel.getInterpolatorType() == InterpolatorType.DEFAULT) {
|
||||
return CombinedChange.createFromApplied(positionChange, timeline.setInterpolatorToDefault(time),
|
||||
timeline.setDefaultInterpolator(interpolator));
|
||||
} else {
|
||||
return CombinedChange.createFromApplied(positionChange, timeline.setInterpolator(time, interpolator));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Position getThis() {
|
||||
return this;
|
||||
}
|
||||
|
||||
public class InterpolationPanel extends AbstractGuiContainer<InterpolationPanel> {
|
||||
|
||||
@Getter
|
||||
private SettingsPanel settingsPanel;
|
||||
|
||||
private GuiDropdownMenu<InterpolatorType> dropdown;
|
||||
|
||||
public InterpolationPanel() {
|
||||
setLayout(new VerticalLayout());
|
||||
|
||||
dropdown = new GuiDropdownMenu<InterpolatorType>()
|
||||
.setToString(s -> I18n.format(s.getI18nName()))
|
||||
.setValues(InterpolatorType.values()).setHeight(20)
|
||||
.onSelection(i -> setSettingsPanel(dropdown.getSelectedValue()));
|
||||
|
||||
// set hover tooltips
|
||||
for (Map.Entry<InterpolatorType, IGuiClickable> e : dropdown.getDropdownEntries().entrySet()) {
|
||||
e.getValue().setTooltip(new GuiTooltip().setI18nText(e.getKey().getI18nDescription()));
|
||||
}
|
||||
|
||||
GuiPanel dropdownPanel = new GuiPanel()
|
||||
.setLayout(new GridLayout().setCellsEqualSize(false).setColumns(2).setSpacingX(3).setSpacingY(5))
|
||||
.addElements(new GridLayout.Data(1, 0.5),
|
||||
new GuiLabel().setI18nText("replaymod.gui.editkeyframe.interpolator"), dropdown);
|
||||
|
||||
|
||||
addElements(new VerticalLayout.Data(0.5, false), dropdownPanel);
|
||||
|
||||
Optional<PathSegment> segment = path.getSegments().stream()
|
||||
.filter(s -> s.getStartKeyframe() == keyframe).findFirst();
|
||||
if (segment.isPresent()) {
|
||||
Interpolator interpolator = segment.get().getInterpolator();
|
||||
InterpolatorType type = InterpolatorType.fromClass(interpolator.getClass());
|
||||
if (keyframe.getValue(ExplicitInterpolationProperty.PROPERTY).isPresent()) {
|
||||
dropdown.setSelected(type); // trigger the callback once to display settings panel
|
||||
} else {
|
||||
setSettingsPanel(InterpolatorType.DEFAULT);
|
||||
}
|
||||
if (getInterpolatorTypeNoDefault(type).getInterpolatorClass().isInstance(interpolator)) {
|
||||
//noinspection unchecked
|
||||
settingsPanel.loadSettings(interpolator);
|
||||
}
|
||||
} else {
|
||||
// Disable dropdown if this is the last keyframe
|
||||
dropdown.setDisabled();
|
||||
}
|
||||
}
|
||||
|
||||
public void setSettingsPanel(InterpolatorType type) {
|
||||
removeElement(this.settingsPanel);
|
||||
|
||||
switch (getInterpolatorTypeNoDefault(type)) {
|
||||
case CATMULL_ROM:
|
||||
settingsPanel = new CatmullRomSettingsPanel();
|
||||
break;
|
||||
case CUBIC:
|
||||
settingsPanel = new CubicSettingsPanel();
|
||||
break;
|
||||
case LINEAR:
|
||||
settingsPanel = new LinearSettingsPanel();
|
||||
break;
|
||||
}
|
||||
|
||||
addElements(new GridLayout.Data(0.5, 0.5), settingsPanel);
|
||||
}
|
||||
|
||||
protected InterpolatorType getInterpolatorTypeNoDefault(InterpolatorType interpolatorType) {
|
||||
if (interpolatorType == InterpolatorType.DEFAULT || interpolatorType == null) {
|
||||
InterpolatorType defaultType = InterpolatorType.fromString(
|
||||
guiPathing.getMod().getCore().getSettingsRegistry().get(Setting.DEFAULT_INTERPOLATION));
|
||||
return defaultType;
|
||||
}
|
||||
return interpolatorType;
|
||||
}
|
||||
|
||||
public InterpolatorType getInterpolatorType() {
|
||||
return dropdown.getSelectedValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected InterpolationPanel getThis() {
|
||||
return this;
|
||||
}
|
||||
|
||||
public abstract class SettingsPanel<I extends Interpolator, T extends SettingsPanel<I, T>> extends AbstractGuiContainer<T> {
|
||||
|
||||
public abstract void loadSettings(I interpolator);
|
||||
|
||||
public abstract I createInterpolator();
|
||||
}
|
||||
|
||||
public class CatmullRomSettingsPanel extends SettingsPanel<CatmullRomSplineInterpolator, CatmullRomSettingsPanel> {
|
||||
public final GuiLabel alphaLabel = new GuiLabel().setColor(Colors.BLACK)
|
||||
.setI18nText("replaymod.gui.editkeyframe.interpolator.catmullrom.alpha");
|
||||
public final GuiNumberField alphaField = new GuiNumberField().setSize(100, 20).setPrecision(5)
|
||||
.setMinValue(0).setValidateOnFocusChange(true);
|
||||
|
||||
{
|
||||
setLayout(new HorizontalLayout(HorizontalLayout.Alignment.CENTER));
|
||||
addElements(new HorizontalLayout.Data(0.5), alphaLabel, alphaField);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadSettings(CatmullRomSplineInterpolator interpolator) {
|
||||
alphaField.setValue(interpolator.getAlpha());
|
||||
}
|
||||
|
||||
@Override
|
||||
public CatmullRomSplineInterpolator createInterpolator() {
|
||||
return new CatmullRomSplineInterpolator(alphaField.getDouble());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CatmullRomSettingsPanel getThis() {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public class CubicSettingsPanel extends SettingsPanel<CubicSplineInterpolator, CubicSettingsPanel> {
|
||||
|
||||
@Override
|
||||
public void loadSettings(CubicSplineInterpolator interpolator) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public CubicSplineInterpolator createInterpolator() {
|
||||
return new CubicSplineInterpolator();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CubicSettingsPanel getThis() {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public class LinearSettingsPanel extends SettingsPanel<LinearInterpolator, LinearSettingsPanel> {
|
||||
|
||||
@Override
|
||||
public void loadSettings(LinearInterpolator interpolator) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public LinearInterpolator createInterpolator() {
|
||||
return new LinearInterpolator();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected LinearSettingsPanel getThis() {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package com.replaymod.simplepathing.gui;
|
||||
|
||||
import com.google.common.base.Function;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.replaymod.core.ReplayMod;
|
||||
import com.replaymod.pathing.properties.CameraProperties;
|
||||
import com.replaymod.pathing.properties.SpectatorProperty;
|
||||
@@ -11,9 +9,10 @@ import com.replaymod.replaystudio.pathing.change.Change;
|
||||
import com.replaymod.replaystudio.pathing.path.Keyframe;
|
||||
import com.replaymod.replaystudio.pathing.path.Path;
|
||||
import com.replaymod.replaystudio.pathing.path.PathSegment;
|
||||
import com.replaymod.replaystudio.pathing.path.Timeline;
|
||||
import com.replaymod.replaystudio.pathing.property.Property;
|
||||
import com.replaymod.simplepathing.ReplayModSimplePathing;
|
||||
import com.replaymod.simplepathing.SPTimeline;
|
||||
import com.replaymod.simplepathing.SPTimeline.SPPath;
|
||||
import de.johni0702.minecraft.gui.GuiRenderer;
|
||||
import de.johni0702.minecraft.gui.element.advanced.AbstractGuiTimeline;
|
||||
import de.johni0702.minecraft.gui.function.Draggable;
|
||||
@@ -23,7 +22,6 @@ import org.lwjgl.util.Point;
|
||||
import org.lwjgl.util.ReadableDimension;
|
||||
import org.lwjgl.util.ReadablePoint;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Comparator;
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -37,14 +35,14 @@ public class GuiKeyframeTimeline extends AbstractGuiTimeline<GuiKeyframeTimeline
|
||||
private final GuiPathing gui;
|
||||
|
||||
/**
|
||||
* The keyframe that was last clicked on using the left mouse button.
|
||||
* The keyframe (time on timeline) that was last clicked on using the left mouse button.
|
||||
*/
|
||||
private Keyframe lastClickedKeyframe;
|
||||
private long lastClickedKeyframe;
|
||||
|
||||
/**
|
||||
* Id of the path of {@link #lastClickedKeyframe}.
|
||||
* Path of {@link #lastClickedKeyframe}.
|
||||
*/
|
||||
private int lastClickedPath;
|
||||
private SPPath lastClickedPath;
|
||||
|
||||
/**
|
||||
* The time at which {@link #lastClickedKeyframe} was updated.
|
||||
@@ -89,18 +87,13 @@ public class GuiKeyframeTimeline extends AbstractGuiTimeline<GuiKeyframeTimeline
|
||||
|
||||
renderer.bindTexture(ReplayMod.TEXTURE);
|
||||
|
||||
for (Keyframe keyframe : Iterables.concat(Iterables.transform(mod.getCurrentTimeline().getPaths(), new Function<Path, Iterable<Keyframe>>() {
|
||||
@Nullable
|
||||
@Override
|
||||
public Iterable<Keyframe> apply(@Nullable Path input) {
|
||||
assert input != null;
|
||||
return input.getKeyframes();
|
||||
}
|
||||
}))) {
|
||||
SPTimeline timeline = mod.getCurrentTimeline();
|
||||
|
||||
timeline.getTimeline().getPaths().stream().flatMap(path -> path.getKeyframes().stream()).forEach(keyframe -> {
|
||||
if (keyframe.getTime() >= startTime && keyframe.getTime() <= endTime) {
|
||||
double relativeTime = keyframe.getTime() - startTime;
|
||||
int positonX = BORDER_LEFT + (int) (relativeTime / visibleTime * visibleWidth) - KEYFRAME_SIZE / 2;
|
||||
int u = KEYFRAME_TEXTURE_X + (mod.getSelectedKeyframe() == keyframe ? KEYFRAME_SIZE : 0);
|
||||
int u = KEYFRAME_TEXTURE_X + (mod.isSelected(keyframe) ? KEYFRAME_SIZE : 0);
|
||||
int v = KEYFRAME_TEXTURE_Y;
|
||||
if (keyframe.getValue(CameraProperties.POSITION).isPresent()) {
|
||||
if (keyframe.getValue(SpectatorProperty.PROPERTY).isPresent()) {
|
||||
@@ -113,10 +106,10 @@ public class GuiKeyframeTimeline extends AbstractGuiTimeline<GuiKeyframeTimeline
|
||||
renderer.drawTexturedRect(positonX, BORDER_TOP + KEYFRAME_SIZE, u, v, KEYFRAME_SIZE, KEYFRAME_SIZE);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Draw colored quads on spectator path segments
|
||||
for (PathSegment segment : mod.getCurrentTimeline().getPaths().get(GuiPathing.POSITION_PATH).getSegments()) {
|
||||
for (PathSegment segment : timeline.getPositionPath().getSegments()) {
|
||||
if (segment.getInterpolator() == null
|
||||
|| !segment.getInterpolator().getKeyframeProperties().contains(SpectatorProperty.PROPERTY)) {
|
||||
continue; // Not a spectator segment
|
||||
@@ -125,7 +118,7 @@ public class GuiKeyframeTimeline extends AbstractGuiTimeline<GuiKeyframeTimeline
|
||||
}
|
||||
|
||||
// Draw red quads on time path segments that would require time going backwards
|
||||
for (PathSegment segment : mod.getCurrentTimeline().getPaths().get(GuiPathing.TIME_PATH).getSegments()) {
|
||||
for (PathSegment segment : timeline.getTimePath().getSegments()) {
|
||||
long startTimestamp = segment.getStartKeyframe().getValue(TimestampProperty.PROPERTY).orElseThrow(IllegalStateException::new);
|
||||
long endTimestamp = segment.getEndKeyframe().getValue(TimestampProperty.PROPERTY).orElseThrow(IllegalStateException::new);
|
||||
if (endTimestamp >= startTimestamp) {
|
||||
@@ -162,29 +155,28 @@ public class GuiKeyframeTimeline extends AbstractGuiTimeline<GuiKeyframeTimeline
|
||||
* @param position The raw position
|
||||
* @return Pair of path id and keyframe or null when no keyframe was clicked
|
||||
*/
|
||||
private Pair<Integer, Keyframe> getKeyframe(ReadablePoint position) {
|
||||
private Pair<SPPath, Long> getKeyframe(ReadablePoint position) {
|
||||
int time = getTimeAt(position.getX(), position.getY());
|
||||
if (time != -1) {
|
||||
Point mouse = new Point(position);
|
||||
getContainer().convertFor(this, mouse);
|
||||
int mouseY = mouse.getY();
|
||||
if (mouseY > BORDER_TOP && mouseY < BORDER_TOP + 2 * KEYFRAME_SIZE) {
|
||||
Timeline timeline = gui.getMod().getCurrentTimeline();
|
||||
int path;
|
||||
SPPath path;
|
||||
if (mouseY <= BORDER_TOP + KEYFRAME_SIZE) {
|
||||
// Position keyframe
|
||||
path = GuiPathing.POSITION_PATH;
|
||||
path = SPPath.POSITION;
|
||||
} else {
|
||||
// Time keyframe
|
||||
path = GuiPathing.TIME_PATH;
|
||||
path = SPPath.TIME;
|
||||
}
|
||||
int visibleTime = (int) (getZoom() * getLength());
|
||||
int tolerance = visibleTime * KEYFRAME_SIZE / (size.getWidth() - BORDER_LEFT - BORDER_RIGHT) / 2;
|
||||
Optional<Keyframe> keyframe = timeline.getPaths().get(path).getKeyframes().stream()
|
||||
int tolerance = visibleTime * KEYFRAME_SIZE / (getLastSize().getWidth() - BORDER_LEFT - BORDER_RIGHT) / 2;
|
||||
Optional<Keyframe> keyframe = gui.getMod().getCurrentTimeline().getPath(path).getKeyframes().stream()
|
||||
.filter(k -> Math.abs(k.getTime() - time) <= tolerance)
|
||||
.sorted(Comparator.comparing(k -> Math.abs(k.getTime() - time)))
|
||||
.findFirst();
|
||||
return Pair.of(path, keyframe.orElse(null));
|
||||
return Pair.of(path, keyframe.map(Keyframe::getTime).orElse(null));
|
||||
}
|
||||
}
|
||||
return Pair.of(null, null);
|
||||
@@ -193,30 +185,31 @@ public class GuiKeyframeTimeline extends AbstractGuiTimeline<GuiKeyframeTimeline
|
||||
@Override
|
||||
public boolean mouseClick(ReadablePoint position, int button) {
|
||||
int time = getTimeAt(position.getX(), position.getY());
|
||||
Pair<Integer, Keyframe> pathKeyframePair = getKeyframe(position);
|
||||
Pair<SPPath, Long> pathKeyframePair = getKeyframe(position);
|
||||
if (pathKeyframePair.getRight() != null) {
|
||||
SPPath path = pathKeyframePair.getLeft();
|
||||
// Clicked on keyframe
|
||||
Keyframe keyframe = pathKeyframePair.getRight();
|
||||
long keyframeTime = pathKeyframePair.getRight();
|
||||
if (button == 0) { // Left click
|
||||
long now = Minecraft.getSystemTime();
|
||||
if (lastClickedKeyframe == keyframe) {
|
||||
if (lastClickedKeyframe == keyframeTime) {
|
||||
// Clicked the same keyframe again, potentially a double click
|
||||
if (now - lastClickedTime < DOUBLE_CLICK_INTERVAL) {
|
||||
// Yup, double click, open the edit keyframe gui
|
||||
Path path = gui.getMod().getCurrentTimeline().getPaths().get(pathKeyframePair.getLeft());
|
||||
gui.openEditKeyframePopup(path, keyframe);
|
||||
gui.openEditKeyframePopup(path, keyframeTime);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Not a double click, just update the click time and selection
|
||||
lastClickedTime = now;
|
||||
lastClickedKeyframe = keyframe;
|
||||
lastClickedPath = pathKeyframePair.getLeft();
|
||||
gui.getMod().setSelectedKeyframe(lastClickedKeyframe);
|
||||
lastClickedKeyframe = keyframeTime;
|
||||
lastClickedPath = path;
|
||||
gui.getMod().setSelected(lastClickedPath, lastClickedKeyframe);
|
||||
// We might be dragging
|
||||
draggingStartX = position.getX();
|
||||
dragging = true;
|
||||
} else if (button == 1) { // Right click
|
||||
Keyframe keyframe = gui.getMod().getCurrentTimeline().getKeyframe(path, keyframeTime);
|
||||
for (Property property : keyframe.getProperties()) {
|
||||
applyPropertyToGame(property, keyframe);
|
||||
}
|
||||
@@ -226,12 +219,11 @@ public class GuiKeyframeTimeline extends AbstractGuiTimeline<GuiKeyframeTimeline
|
||||
// Clicked on timeline but not on any keyframe
|
||||
if (button == 0) { // Left click
|
||||
setCursorPosition(time);
|
||||
gui.getMod().setSelectedKeyframe(null);
|
||||
gui.getMod().setSelected(null, 0);
|
||||
} else if (button == 1) { // Right click
|
||||
if (pathKeyframePair.getLeft() != null) {
|
||||
// Apply the value of the clicked path at the clicked position
|
||||
Timeline timeline = gui.getMod().getCurrentTimeline();
|
||||
Path path = timeline.getPaths().get(pathKeyframePair.getLeft());
|
||||
Path path = gui.getMod().getCurrentTimeline().getPath(pathKeyframePair.getLeft());
|
||||
path.getKeyframes().stream().flatMap(k -> k.getProperties().stream()).distinct().forEach(
|
||||
p -> applyPropertyToGame(p, path, time));
|
||||
}
|
||||
@@ -280,12 +272,13 @@ public class GuiKeyframeTimeline extends AbstractGuiTimeline<GuiKeyframeTimeline
|
||||
}
|
||||
}
|
||||
if (actuallyDragging) {
|
||||
if (!gui.ensureEntityTracker(() -> mouseDrag(position, button, timeSinceLastCall))) return true;
|
||||
// Threshold passed
|
||||
Path path = gui.getMod().getCurrentTimeline().getPaths().get(lastClickedPath);
|
||||
SPTimeline timeline = gui.getMod().getCurrentTimeline();
|
||||
Point mouse = new Point(position);
|
||||
getContainer().convertFor(this, mouse);
|
||||
int mouseX = mouse.getX();
|
||||
int width = size.getWidth();
|
||||
int width = getLastSize().getWidth();
|
||||
int bodyWidth = width - BORDER_LEFT - BORDER_RIGHT;
|
||||
double segmentLength = getLength() * getZoom();
|
||||
double segmentTime = segmentLength * (mouseX - BORDER_LEFT) / bodyWidth;
|
||||
@@ -295,24 +288,21 @@ public class GuiKeyframeTimeline extends AbstractGuiTimeline<GuiKeyframeTimeline
|
||||
}
|
||||
|
||||
// If there already is a keyframe at the target time, then increase the time by one until there is none
|
||||
while (path.getKeyframe(newTime) != null) {
|
||||
while (timeline.getKeyframe(lastClickedPath, newTime) != null) {
|
||||
newTime++;
|
||||
}
|
||||
|
||||
// First undo any previous changes
|
||||
if (draggingChange != null) {
|
||||
draggingChange.undo(gui.getMod().getCurrentTimeline());
|
||||
draggingChange.undo(timeline.getTimeline());
|
||||
}
|
||||
|
||||
// Move keyframe to new position and
|
||||
// store change for later undoing / pushing to history
|
||||
draggingChange = gui.moveKeyframe(path, lastClickedKeyframe, newTime);
|
||||
draggingChange = timeline.moveKeyframe(lastClickedPath, lastClickedKeyframe, newTime);
|
||||
|
||||
// Selected keyframe has been replaced
|
||||
gui.getMod().setSelectedKeyframe(path.getKeyframe(newTime));
|
||||
|
||||
// Path has been changed
|
||||
path.updateAll();
|
||||
gui.getMod().setSelected(lastClickedPath, newTime);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -321,7 +311,7 @@ public class GuiKeyframeTimeline extends AbstractGuiTimeline<GuiKeyframeTimeline
|
||||
public boolean mouseRelease(ReadablePoint position, int button) {
|
||||
if (dragging) {
|
||||
if (actuallyDragging) {
|
||||
gui.getMod().getCurrentTimeline().pushChange(draggingChange);
|
||||
gui.getMod().getCurrentTimeline().getTimeline().pushChange(draggingChange);
|
||||
draggingChange = null;
|
||||
actuallyDragging = false;
|
||||
}
|
||||
|
||||
@@ -14,25 +14,26 @@ import com.replaymod.render.gui.GuiRenderSettings;
|
||||
import com.replaymod.replay.ReplayHandler;
|
||||
import com.replaymod.replay.camera.CameraEntity;
|
||||
import com.replaymod.replay.gui.overlay.GuiReplayOverlay;
|
||||
import com.replaymod.replaystudio.pathing.change.*;
|
||||
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.pathing.serialize.TimelineSerialization;
|
||||
import com.replaymod.replaystudio.util.EntityPositionTracker;
|
||||
import com.replaymod.replaystudio.util.Location;
|
||||
import com.replaymod.simplepathing.ReplayModSimplePathing;
|
||||
import com.replaymod.simplepathing.Setting;
|
||||
import com.replaymod.simplepathing.SPTimeline;
|
||||
import com.replaymod.simplepathing.SPTimeline.SPPath;
|
||||
import de.johni0702.minecraft.gui.GuiRenderer;
|
||||
import de.johni0702.minecraft.gui.RenderInfo;
|
||||
import de.johni0702.minecraft.gui.container.GuiContainer;
|
||||
import de.johni0702.minecraft.gui.container.GuiPanel;
|
||||
import de.johni0702.minecraft.gui.element.*;
|
||||
import de.johni0702.minecraft.gui.element.AbstractGuiClickable;
|
||||
import de.johni0702.minecraft.gui.element.AbstractGuiElement;
|
||||
import de.johni0702.minecraft.gui.element.GuiElement;
|
||||
import de.johni0702.minecraft.gui.element.GuiHorizontalScrollbar;
|
||||
import de.johni0702.minecraft.gui.element.GuiLabel;
|
||||
import de.johni0702.minecraft.gui.element.GuiTexturedButton;
|
||||
import de.johni0702.minecraft.gui.element.GuiTooltip;
|
||||
import de.johni0702.minecraft.gui.element.IGuiClickable;
|
||||
import de.johni0702.minecraft.gui.element.advanced.GuiProgressBar;
|
||||
import de.johni0702.minecraft.gui.element.advanced.GuiTimelineTime;
|
||||
import de.johni0702.minecraft.gui.layout.CustomLayout;
|
||||
@@ -43,9 +44,7 @@ import de.johni0702.minecraft.gui.popup.GuiInfoPopup;
|
||||
import de.johni0702.minecraft.gui.popup.GuiYesNoPopup;
|
||||
import de.johni0702.minecraft.gui.utils.Colors;
|
||||
import net.minecraft.crash.CrashReport;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraftforge.fml.common.Loader;
|
||||
import org.apache.commons.lang3.tuple.Triple;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.lwjgl.input.Keyboard;
|
||||
@@ -57,10 +56,7 @@ import org.lwjgl.util.WritablePoint;
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static com.replaymod.core.utils.Utils.error;
|
||||
@@ -71,9 +67,6 @@ import static com.replaymod.simplepathing.ReplayModSimplePathing.LOGGER;
|
||||
* Gui plug-in to the GuiReplayOverlay for simple pathing.
|
||||
*/
|
||||
public class GuiPathing {
|
||||
public static final int TIME_PATH = 0;
|
||||
public static final int POSITION_PATH = 1;
|
||||
|
||||
private static final Logger logger = LogManager.getLogger();
|
||||
|
||||
public final GuiTexturedButton playPauseButton = new GuiTexturedButton() {
|
||||
@@ -99,10 +92,11 @@ public class GuiPathing {
|
||||
if (!preparePathsForPlayback()) return;
|
||||
|
||||
// Clone the timeline passed to the settings gui as it may be stored for later rendering in a queue
|
||||
Timeline timeline = mod.getCurrentTimeline();
|
||||
SPTimeline spTimeline = mod.getCurrentTimeline();
|
||||
Timeline timeline;
|
||||
try {
|
||||
TimelineSerialization serialization = new TimelineSerialization(mod, null);
|
||||
String serialized = serialization.serialize(Collections.singletonMap("", timeline));
|
||||
TimelineSerialization serialization = new TimelineSerialization(spTimeline, null);
|
||||
String serialized = serialization.serialize(Collections.singletonMap("", spTimeline.getTimeline()));
|
||||
timeline = serialization.deserialize(serialized).get("");
|
||||
} catch (Throwable t) {
|
||||
error(LOGGER, replayHandler.getOverlay(), CrashReport.makeCrashReport(t, "Cloning timeline"), () -> {});
|
||||
@@ -296,27 +290,28 @@ public class GuiPathing {
|
||||
if (player.isActive()) {
|
||||
player.getFuture().cancel(false);
|
||||
} else {
|
||||
Timeline timeline = mod.getCurrentTimeline();
|
||||
Path timePath = timeline.getPaths().get(TIME_PATH);
|
||||
Path timePath = mod.getCurrentTimeline().getTimePath();
|
||||
|
||||
if (!preparePathsForPlayback()) return;
|
||||
|
||||
timePath.setActive(!Keyboard.isKeyDown(Keyboard.KEY_LSHIFT));
|
||||
// Start from cursor time unless the control key is pressed (then start from beginning)
|
||||
int startTime = Keyboard.isKeyDown(Keyboard.KEY_LCONTROL)? 0 : GuiPathing.this.timeline.getCursorPosition();
|
||||
ListenableFuture<Void> future = player.start(timeline, startTime);
|
||||
ListenableFuture<Void> future = player.start(mod.getCurrentTimeline().getTimeline(), startTime);
|
||||
overlay.setCloseable(false);
|
||||
overlay.setMouseVisible(true);
|
||||
Futures.addCallback(future, new FutureCallback<Void>() {
|
||||
@Override
|
||||
public void onSuccess(@Nullable Void result) {
|
||||
overlay.setCloseable(true);
|
||||
timePath.setActive(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable t) {
|
||||
t.printStackTrace();
|
||||
overlay.setCloseable(true);
|
||||
timePath.setActive(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -326,26 +321,28 @@ public class GuiPathing {
|
||||
positionKeyframeButton.setTexturePosH(new ReadablePoint() {
|
||||
@Override
|
||||
public int getX() {
|
||||
Keyframe keyframe = mod.getSelectedKeyframe();
|
||||
if (keyframe == null || !keyframe.getValue(CameraProperties.POSITION).isPresent()) {
|
||||
SPPath keyframePath = mod.getSelectedPath();
|
||||
long keyframeTime = mod.getSelectedTime();
|
||||
if (keyframePath != SPPath.POSITION) {
|
||||
// No keyframe or wrong path
|
||||
keyframe = mod.getCurrentTimeline().getPaths().get(POSITION_PATH).getKeyframe(timeline.getCursorPosition());
|
||||
keyframeTime = timeline.getCursorPosition();
|
||||
keyframePath = mod.getCurrentTimeline().isPositionKeyframe(keyframeTime) ? SPPath.POSITION : null;
|
||||
}
|
||||
if (keyframe == null) {
|
||||
if (keyframePath != SPPath.POSITION) {
|
||||
return replayHandler.isCameraView() ? 0 : 40;
|
||||
} else {
|
||||
return keyframe.getValue(SpectatorProperty.PROPERTY).isPresent() ? 40 : 0;
|
||||
return mod.getCurrentTimeline().isSpectatorKeyframe(keyframeTime) ? 40 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getY() {
|
||||
Keyframe keyframe = mod.getSelectedKeyframe();
|
||||
if (keyframe == null || !keyframe.getValue(CameraProperties.POSITION).isPresent()) {
|
||||
SPPath keyframePath = mod.getSelectedPath();
|
||||
if (keyframePath != SPPath.POSITION) {
|
||||
// No keyframe selected but there might be one at exactly the position of the cursor
|
||||
keyframe = mod.getCurrentTimeline().getPaths().get(POSITION_PATH).getKeyframe(timeline.getCursorPosition());
|
||||
keyframePath = mod.getCurrentTimeline().isPositionKeyframe(timeline.getCursorPosition()) ? SPPath.POSITION : null;
|
||||
}
|
||||
return keyframe != null && keyframe.getValue(CameraProperties.POSITION).isPresent() ? 60 : 40;
|
||||
return keyframePath == SPPath.POSITION ? 60 : 40;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -355,7 +352,7 @@ public class GuiPathing {
|
||||
}).onClick(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
updateKeyframe(false);
|
||||
updateKeyframe(SPPath.POSITION);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -367,12 +364,12 @@ public class GuiPathing {
|
||||
|
||||
@Override
|
||||
public int getY() {
|
||||
Keyframe keyframe = mod.getSelectedKeyframe();
|
||||
if (keyframe == null || !keyframe.getValue(TimestampProperty.PROPERTY).isPresent()) {
|
||||
SPPath keyframePath = mod.getSelectedPath();
|
||||
if (keyframePath != SPPath.TIME) {
|
||||
// No keyframe selected but there might be one at exactly the position of the cursor
|
||||
keyframe = mod.getCurrentTimeline().getPaths().get(TIME_PATH).getKeyframe(timeline.getCursorPosition());
|
||||
keyframePath = mod.getCurrentTimeline().isTimeKeyframe(timeline.getCursorPosition()) ? SPPath.TIME : null;
|
||||
}
|
||||
return keyframe != null && keyframe.getValue(TimestampProperty.PROPERTY).isPresent() ? 100 : 80;
|
||||
return keyframePath == SPPath.TIME ? 100 : 80;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -382,7 +379,7 @@ public class GuiPathing {
|
||||
}).onClick(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
updateKeyframe(true);
|
||||
updateKeyframe(SPPath.TIME);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -404,12 +401,12 @@ public class GuiPathing {
|
||||
}
|
||||
try {
|
||||
GuiKeyframeRepository gui = new GuiKeyframeRepository(
|
||||
mod, replayHandler.getReplayFile(), mod.getCurrentTimeline());
|
||||
mod.getCurrentTimeline(), replayHandler.getReplayFile(), mod.getCurrentTimeline().getTimeline());
|
||||
Futures.addCallback(gui.getFuture(), new FutureCallback<Timeline>() {
|
||||
@Override
|
||||
public void onSuccess(Timeline result) {
|
||||
if (result != null) {
|
||||
mod.setCurrentTimeline(result);
|
||||
mod.setCurrentTimeline(new SPTimeline(result));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -435,10 +432,10 @@ public class GuiPathing {
|
||||
@Override
|
||||
public void onSuccess(Boolean delete) {
|
||||
if (delete) {
|
||||
Timeline timeline = mod.createTimeline();
|
||||
timeline.createPath();
|
||||
timeline.createPath();
|
||||
mod.setCurrentTimeline(timeline);
|
||||
mod.clearCurrentTimeline();
|
||||
if (entityTracker != null) {
|
||||
mod.getCurrentTimeline().setEntityTracker(entityTracker);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -455,7 +452,7 @@ public class GuiPathing {
|
||||
// Position of the cursor
|
||||
int cursor = timeline.getCursorPosition();
|
||||
// Get the last time keyframe before the cursor
|
||||
mod.getCurrentTimeline().getPaths().get(TIME_PATH).getKeyframes().stream()
|
||||
mod.getCurrentTimeline().getTimePath().getKeyframes().stream()
|
||||
.filter(it -> it.getTime() <= cursor).reduce((__, last) -> last).ifPresent(keyframe -> {
|
||||
// Cursor position at the keyframe
|
||||
int keyframeCursor = (int) keyframe.getTime();
|
||||
@@ -471,7 +468,7 @@ public class GuiPathing {
|
||||
// Move cursor to new position
|
||||
timeline.setCursorPosition(keyframeCursor + cursorPassed);
|
||||
// Deselect keyframe to allow the user to add a new one right away
|
||||
mod.setSelectedKeyframe(null);
|
||||
mod.setSelected(null, 0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -479,8 +476,8 @@ public class GuiPathing {
|
||||
if (!overlay.isVisible()) {
|
||||
return;
|
||||
}
|
||||
if (mod.getSelectedKeyframe() != null) {
|
||||
updateKeyframe(mod.getSelectedKeyframe().getValue(TimestampProperty.PROPERTY).isPresent());
|
||||
if (mod.getSelectedPath() != null) {
|
||||
updateKeyframe(mod.getSelectedPath());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -511,13 +508,12 @@ public class GuiPathing {
|
||||
}
|
||||
|
||||
private boolean preparePathsForPlayback() {
|
||||
Timeline timeline = mod.getCurrentTimeline();
|
||||
timeline.getPaths().get(TIME_PATH).updateAll();
|
||||
timeline.getPaths().get(POSITION_PATH).updateAll();
|
||||
SPTimeline timeline = mod.getCurrentTimeline();
|
||||
timeline.getTimeline().getPaths().forEach(Path::updateAll);
|
||||
|
||||
// Make sure time keyframes's values are monotonically increasing
|
||||
int lastTime = 0;
|
||||
for (Keyframe keyframe : timeline.getPaths().get(TIME_PATH).getKeyframes()) {
|
||||
for (Keyframe keyframe : timeline.getTimePath().getKeyframes()) {
|
||||
int time = keyframe.getValue(TimestampProperty.PROPERTY).orElseThrow(IllegalStateException::new);
|
||||
if (time < lastTime) {
|
||||
// We are going backwards in time
|
||||
@@ -531,8 +527,8 @@ public class GuiPathing {
|
||||
}
|
||||
|
||||
// Make sure there are at least two position- and two time-keyframes
|
||||
if (timeline.getPaths().get(POSITION_PATH).getSegments().isEmpty()
|
||||
|| timeline.getPaths().get(TIME_PATH).getSegments().isEmpty()) {
|
||||
if (timeline.getPositionPath().getSegments().isEmpty()
|
||||
|| timeline.getTimePath().getSegments().isEmpty()) {
|
||||
GuiInfoPopup.open(replayHandler.getOverlay(), "replaymod.chat.morekeyframes");
|
||||
return false;
|
||||
}
|
||||
@@ -544,19 +540,19 @@ public class GuiPathing {
|
||||
scrollbar.setZoom(scrollbar.getZoom() * factor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when either one of the property buttons is pressed.
|
||||
* @param isTime {@code true} for the time property button, {@code false} for the place property button
|
||||
*/
|
||||
private void updateKeyframe(final boolean isTime) {
|
||||
public boolean ensureEntityTracker(Runnable withDelayedTracker) {
|
||||
if (entityTracker == null) {
|
||||
LOGGER.debug("Entity tracker not yet loaded, delaying...");
|
||||
LoadEntityTrackerPopup popup = new LoadEntityTrackerPopup(replayHandler.getOverlay());
|
||||
entityTrackerLoadingProgress = p -> popup.progressBar.setProgress(p.floatValue());
|
||||
Futures.addCallback(entityTrackerFuture, new FutureCallback<Void>() {
|
||||
@Override
|
||||
public void onSuccess(@Nullable Void result) {
|
||||
popup.close();
|
||||
updateKeyframe(isTime);
|
||||
if (mod.getCurrentTimeline().getEntityTracker() == null) {
|
||||
mod.getCurrentTimeline().setEntityTracker(entityTracker);
|
||||
}
|
||||
withDelayedTracker.run();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -564,184 +560,63 @@ public class GuiPathing {
|
||||
popup.close();
|
||||
}
|
||||
});
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
if (mod.getCurrentTimeline().getEntityTracker() == null) {
|
||||
mod.getCurrentTimeline().setEntityTracker(entityTracker);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when either one of the property buttons is pressed.
|
||||
* @param path {@code TIME} for the time property button, {@code POSITION} for the place property button
|
||||
*/
|
||||
private void updateKeyframe(SPPath path) {
|
||||
LOGGER.debug("Updating keyframe on path {}" + path);
|
||||
if (!ensureEntityTracker(() -> updateKeyframe(path))) return;
|
||||
|
||||
int time = timeline.getCursorPosition();
|
||||
Timeline timeline = mod.getCurrentTimeline();
|
||||
Path path = timeline.getPaths().get(isTime ? TIME_PATH : POSITION_PATH);
|
||||
SPTimeline timeline = mod.getCurrentTimeline();
|
||||
|
||||
Keyframe keyframe = mod.getSelectedKeyframe();
|
||||
if (keyframe != null && keyframe.getValue(TimestampProperty.PROPERTY).isPresent() ^ isTime) {
|
||||
// Keyframe is on the wrong timeline
|
||||
keyframe = null;
|
||||
}
|
||||
if (keyframe == null) {
|
||||
// No keyframe selected but there may still be one at this exact time
|
||||
keyframe = path.getKeyframe(time);
|
||||
}
|
||||
Change change;
|
||||
if (keyframe == null) {
|
||||
change = AddKeyframe.create(path, time);
|
||||
change.apply(timeline);
|
||||
keyframe = path.getKeyframe(time);
|
||||
} else {
|
||||
change = RemoveKeyframe.create(path, keyframe);
|
||||
change.apply(timeline);
|
||||
keyframe = null;
|
||||
}
|
||||
|
||||
if (keyframe != null) {
|
||||
UpdateKeyframeProperties.Builder builder = UpdateKeyframeProperties.create(path, keyframe);
|
||||
if (isTime) {
|
||||
builder.setValue(TimestampProperty.PROPERTY, replayHandler.getReplaySender().currentTimeStamp());
|
||||
} else {
|
||||
CameraEntity camera = replayHandler.getCameraEntity();
|
||||
builder.setValue(CameraProperties.POSITION, Triple.of(camera.posX, camera.posY, camera.posZ));
|
||||
builder.setValue(CameraProperties.ROTATION, Triple.of(camera.rotationYaw, camera.rotationPitch, camera.roll));
|
||||
if (!replayHandler.isCameraView()) {
|
||||
Entity spectated = replayHandler.getOverlay().getMinecraft().getRenderViewEntity();
|
||||
builder.setValue(SpectatorProperty.PROPERTY, spectated.getEntityId());
|
||||
switch (path) {
|
||||
case TIME:
|
||||
if (mod.getSelectedPath() == path) {
|
||||
LOGGER.debug("Selected keyframe is time keyframe -> removing keyframe");
|
||||
timeline.removeTimeKeyframe(mod.getSelectedTime());
|
||||
mod.setSelected(null, 0);
|
||||
} else if (timeline.isTimeKeyframe(time)) {
|
||||
LOGGER.debug("Keyframe at cursor position is time keyframe -> removing keyframe");
|
||||
timeline.removeTimeKeyframe(time);
|
||||
mod.setSelected(null, 0);
|
||||
} else {
|
||||
LOGGER.debug("No time keyframe found -> adding new keyframe");
|
||||
timeline.addTimeKeyframe(time, replayHandler.getReplaySender().currentTimeStamp());
|
||||
mod.setSelected(path, time);
|
||||
}
|
||||
}
|
||||
UpdateKeyframeProperties updateChange = builder.done();
|
||||
updateChange.apply(timeline);
|
||||
change = CombinedChange.createFromApplied(change, updateChange);
|
||||
|
||||
// If this new keyframe formed the first segment of the time path
|
||||
if (isTime && path.getSegments().size() == 1) {
|
||||
PathSegment segment = path.getSegments().iterator().next();
|
||||
Interpolator interpolator = new LinearInterpolator();
|
||||
interpolator.registerProperty(TimestampProperty.PROPERTY);
|
||||
SetInterpolator setInterpolator = SetInterpolator.create(segment, interpolator);
|
||||
setInterpolator.apply(timeline);
|
||||
change = CombinedChange.createFromApplied(change, setInterpolator);
|
||||
}
|
||||
}
|
||||
|
||||
// Update interpolators for spectator keyframes
|
||||
// while this is overkill, it is far simpler than updating differently for every possible case
|
||||
if (!isTime) {
|
||||
Change interpolators = updateInterpolators();
|
||||
interpolators.apply(timeline);
|
||||
change = CombinedChange.createFromApplied(change, interpolators);
|
||||
}
|
||||
|
||||
Change specPosUpdate = updateSpectatorPositions();
|
||||
specPosUpdate.apply(timeline);
|
||||
change = CombinedChange.createFromApplied(change, specPosUpdate);
|
||||
|
||||
timeline.pushChange(change);
|
||||
|
||||
mod.setSelectedKeyframe(keyframe);
|
||||
}
|
||||
|
||||
public Change updateInterpolators() {
|
||||
boolean linearInterpolation = mod.getCore().getSettingsRegistry().get(Setting.LINEAR_INTERPOLATION);
|
||||
List<Change> changes = new ArrayList<>();
|
||||
Interpolator interpolator = null;
|
||||
boolean isSpectatorInterpolator = false;
|
||||
for (PathSegment segment : mod.getCurrentTimeline().getPaths().get(POSITION_PATH).getSegments()) {
|
||||
if (segment.getStartKeyframe().getValue(SpectatorProperty.PROPERTY).isPresent()
|
||||
&& segment.getEndKeyframe().getValue(SpectatorProperty.PROPERTY).isPresent()) {
|
||||
// Spectator segment
|
||||
if (!isSpectatorInterpolator) {
|
||||
isSpectatorInterpolator = true;
|
||||
interpolator = new LinearInterpolator();
|
||||
interpolator.registerProperty(SpectatorProperty.PROPERTY);
|
||||
break;
|
||||
case POSITION:
|
||||
if (mod.getSelectedPath() == path) {
|
||||
LOGGER.debug("Selected keyframe is position keyframe -> removing keyframe");
|
||||
timeline.removePositionKeyframe(mod.getSelectedTime());
|
||||
mod.setSelected(null, 0);
|
||||
} else if (timeline.isPositionKeyframe(time)) {
|
||||
LOGGER.debug("Keyframe at cursor position is position keyframe -> removing keyframe");
|
||||
timeline.removePositionKeyframe(time);
|
||||
mod.setSelected(null, 0);
|
||||
} else {
|
||||
LOGGER.debug("No position keyframe found -> adding new keyframe");
|
||||
CameraEntity camera = replayHandler.getCameraEntity();
|
||||
int spectatedId = -1;
|
||||
if (!replayHandler.isCameraView()) {
|
||||
spectatedId = replayHandler.getOverlay().getMinecraft().getRenderViewEntity().getEntityId();
|
||||
}
|
||||
timeline.addPositionKeyframe(time, camera.posX, camera.posY, camera.posZ,
|
||||
camera.rotationYaw, camera.rotationPitch, camera.roll, spectatedId);
|
||||
mod.setSelected(path, time);
|
||||
}
|
||||
changes.add(SetInterpolator.create(segment, interpolator));
|
||||
} else {
|
||||
// Normal segment
|
||||
if (isSpectatorInterpolator || interpolator == null) {
|
||||
isSpectatorInterpolator = false;
|
||||
interpolator = linearInterpolation ? new LinearInterpolator() : new CubicSplineInterpolator();
|
||||
interpolator.registerProperty(CameraProperties.POSITION);
|
||||
interpolator.registerProperty(CameraProperties.ROTATION);
|
||||
}
|
||||
changes.add(SetInterpolator.create(segment, interpolator));
|
||||
}
|
||||
break;
|
||||
}
|
||||
return CombinedChange.create(changes.toArray(new Change[changes.size()]));
|
||||
}
|
||||
|
||||
public Change updateSpectatorPositions() {
|
||||
List<Change> changes = new ArrayList<>();
|
||||
Path positionPath = mod.getCurrentTimeline().getPaths().get(POSITION_PATH);
|
||||
Path timePath = mod.getCurrentTimeline().getPaths().get(TIME_PATH);
|
||||
timePath.updateAll();
|
||||
for (Keyframe keyframe : positionPath.getKeyframes()) {
|
||||
Optional<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()]));
|
||||
}
|
||||
|
||||
public Change moveKeyframe(Path path, Keyframe keyframe, long newTime) {
|
||||
Timeline timeline = mod.getCurrentTimeline();
|
||||
// Interpolator might be required later (only if path is the time path)
|
||||
Optional<Interpolator> interpolator =
|
||||
path.getSegments().stream().findFirst().map(PathSegment::getInterpolator);
|
||||
|
||||
// First remove the old keyframe
|
||||
Change removeChange = RemoveKeyframe.create(path, keyframe);
|
||||
removeChange.apply(timeline);
|
||||
|
||||
// and add a new one at the correct time
|
||||
Change addChange = AddKeyframe.create(path, newTime);
|
||||
addChange.apply(timeline);
|
||||
path.getKeyframe(newTime);
|
||||
|
||||
// Then copy over all properties
|
||||
UpdateKeyframeProperties.Builder builder = UpdateKeyframeProperties.create(path, path.getKeyframe(newTime));
|
||||
for (Property property : keyframe.getProperties()) {
|
||||
copyProperty(property, keyframe, builder);
|
||||
}
|
||||
Change propertyChange = builder.done();
|
||||
propertyChange.apply(timeline);
|
||||
|
||||
// Finally set the interpolators
|
||||
Change interpolatorChange;
|
||||
if (path.getTimeline().getPaths().indexOf(path) == GuiPathing.POSITION_PATH) {
|
||||
// Position / Spectator keyframes need special handling
|
||||
interpolatorChange = updateInterpolators();
|
||||
} else {
|
||||
// Time keyframes only need updating when only one segment of them exists
|
||||
if (path.getSegments().size() == 1) {
|
||||
interpolatorChange = SetInterpolator.create(path.getSegments().iterator().next(), interpolator.get());
|
||||
} else {
|
||||
interpolatorChange = CombinedChange.create(); // Noop change
|
||||
}
|
||||
}
|
||||
interpolatorChange.apply(timeline);
|
||||
// and update spectator positions
|
||||
Change spectatorChange = updateSpectatorPositions();
|
||||
spectatorChange.apply(timeline);
|
||||
|
||||
return CombinedChange.createFromApplied(removeChange, addChange, propertyChange, interpolatorChange, 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));
|
||||
}
|
||||
|
||||
public ReplayModSimplePathing getMod() {
|
||||
@@ -752,13 +627,15 @@ public class GuiPathing {
|
||||
return entityTracker;
|
||||
}
|
||||
|
||||
public void openEditKeyframePopup(Path path, Keyframe keyframe) {
|
||||
public void openEditKeyframePopup(SPPath path, long time) {
|
||||
if (!ensureEntityTracker(() -> openEditKeyframePopup(path, time))) return;
|
||||
Keyframe keyframe = mod.getCurrentTimeline().getKeyframe(path, time);
|
||||
if (keyframe.getProperties().contains(SpectatorProperty.PROPERTY)) {
|
||||
new GuiEditKeyframe.Spectator(this, path, keyframe).open();
|
||||
new GuiEditKeyframe.Spectator(this, path, keyframe.getTime()).open();
|
||||
} else if (keyframe.getProperties().contains(CameraProperties.POSITION)) {
|
||||
new GuiEditKeyframe.Position(this, path, keyframe).open();
|
||||
new GuiEditKeyframe.Position(this, path, keyframe.getTime()).open();
|
||||
} else {
|
||||
new GuiEditKeyframe.Time(this, path, keyframe).open();
|
||||
new GuiEditKeyframe.Time(this, path, keyframe.getTime()).open();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,10 +9,10 @@ import com.replaymod.replaystudio.pathing.interpolation.Interpolator;
|
||||
import com.replaymod.replaystudio.pathing.path.Keyframe;
|
||||
import com.replaymod.replaystudio.pathing.path.Path;
|
||||
import com.replaymod.replaystudio.pathing.path.PathSegment;
|
||||
import com.replaymod.replaystudio.pathing.path.Timeline;
|
||||
import com.replaymod.replaystudio.util.EntityPositionTracker;
|
||||
import com.replaymod.replaystudio.util.Location;
|
||||
import com.replaymod.simplepathing.ReplayModSimplePathing;
|
||||
import com.replaymod.simplepathing.SPTimeline;
|
||||
import com.replaymod.simplepathing.gui.GuiPathing;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.GlStateManager;
|
||||
@@ -68,11 +68,11 @@ public class PathPreviewRenderer {
|
||||
if (guiPathing == null) return;
|
||||
EntityPositionTracker entityTracker = guiPathing.getEntityTracker();
|
||||
|
||||
Timeline timeline = mod.getCurrentTimeline();
|
||||
SPTimeline timeline = mod.getCurrentTimeline();
|
||||
if (timeline == null) return;
|
||||
Path path = timeline.getPaths().get(GuiPathing.POSITION_PATH);
|
||||
Path path = timeline.getPositionPath();
|
||||
if (path.getKeyframes().isEmpty()) return;
|
||||
Path timePath = timeline.getPaths().get(GuiPathing.TIME_PATH);
|
||||
Path timePath = timeline.getTimePath();
|
||||
|
||||
path.update();
|
||||
|
||||
@@ -97,8 +97,8 @@ public class PathPreviewRenderer {
|
||||
if (spectator && entityTracker == null) {
|
||||
continue; // Cannot render spectator positions when entity tracker is not yet loaded
|
||||
}
|
||||
// Spectator segments have 20 lines per second whereas normal segments have a fixed 100
|
||||
long steps = spectator ? diff / 50 : 100;
|
||||
// Spectator segments have 20 lines per second (at least 10) whereas normal segments have a fixed 100
|
||||
long steps = spectator ? Math.max(diff / 50, 10) : 100;
|
||||
Triple<Double, Double, Double> prevPos = null;
|
||||
for (int i = 0; i <= steps; i++) {
|
||||
long time = start.getTime() + diff * i / steps;
|
||||
@@ -240,7 +240,7 @@ public class PathPreviewRenderer {
|
||||
float posY = 0f;
|
||||
float size = 10f / ReplayMod.TEXTURE_SIZE;
|
||||
|
||||
if (keyframe == mod.getSelectedKeyframe()) {
|
||||
if (mod.isSelected(keyframe)) {
|
||||
posY += size;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.replaymod.simplepathing.properties;
|
||||
|
||||
import com.google.gson.stream.JsonReader;
|
||||
import com.google.gson.stream.JsonWriter;
|
||||
import com.replaymod.replaystudio.pathing.property.AbstractProperty;
|
||||
import com.replaymod.replaystudio.pathing.property.PropertyPart;
|
||||
import lombok.NonNull;
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* Property indicating that the next path segment's interpolator is fixed
|
||||
*/
|
||||
public class ExplicitInterpolationProperty extends AbstractProperty<Object> {
|
||||
public static final ExplicitInterpolationProperty PROPERTY = new ExplicitInterpolationProperty();
|
||||
|
||||
private ExplicitInterpolationProperty() {
|
||||
super("interpolationFixed", "<internal>", null, new Object());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<PropertyPart<Object>> getParts() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyToGame(Object value, @NonNull Object replayHandler) {
|
||||
// dummy property, do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void toJson(JsonWriter writer, Object value) throws IOException {
|
||||
writer.nullValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object fromJson(JsonReader reader) throws IOException {
|
||||
reader.nextNull();
|
||||
return ObjectUtils.NULL;
|
||||
}
|
||||
}
|
||||
@@ -292,6 +292,7 @@ replaymod.gui.settings.title=Replay Mod Settings
|
||||
|
||||
replaymod.gui.settings.interpolation.linear=Linear
|
||||
replaymod.gui.settings.interpolation.cubic=Cubic
|
||||
replaymod.gui.settings.interpolation.catmullrom=Catmull Rom
|
||||
|
||||
replaymod.gui.settings.bitrate=Video Bitrate
|
||||
replaymod.gui.settings.framerate=Video Framerate
|
||||
@@ -309,6 +310,7 @@ replaymod.gui.settings.keyframecleancallback=Clear Confirmation
|
||||
replaymod.gui.settings.renderinvisible=Render invisible Entities
|
||||
replaymod.gui.settings.camera=Camera
|
||||
replaymod.gui.settings.showchat=Show Chat
|
||||
replaymod.gui.settings.interpolator=Default Interpolator
|
||||
|
||||
replaymod.gui.settings.warning.linea=WARNING: Recording settings will be
|
||||
replaymod.gui.settings.warning.lineb=applied the next time you join a world.
|
||||
@@ -372,6 +374,17 @@ replaymod.gui.editkeyframe.spec.method.shoulder.pitch=Pitch Offset
|
||||
replaymod.gui.editkeyframe.spec.method.shoulder.yaw=Rotation Angle
|
||||
replaymod.gui.editkeyframe.spec.method.shoulder.smoothness=Path Smoothness
|
||||
|
||||
replaymod.gui.editkeyframe.interpolator=Interpolator
|
||||
replaymod.gui.editkeyframe.interpolator.default.name=Default Interpolator
|
||||
replaymod.gui.editkeyframe.interpolator.default.desc=Uses the default interpolator defined in the Replay Mod Settings.
|
||||
replaymod.gui.editkeyframe.interpolator.catmullrom.name=Catmull Rom Spline Interpolator
|
||||
replaymod.gui.editkeyframe.interpolator.catmullrom.desc=Calculates a catmull rom spline with customizable alpha value for precise tightness and smoothness of the camera path.
|
||||
replaymod.gui.editkeyframe.interpolator.catmullrom.alpha=Alpha:
|
||||
replaymod.gui.editkeyframe.interpolator.cubic.name=Cubic Spline Interpolator
|
||||
replaymod.gui.editkeyframe.interpolator.cubic.desc=Calculates a cubic equation matrix for all points for a smooth camera path.
|
||||
replaymod.gui.editkeyframe.interpolator.linear.name=Linear Interpolator
|
||||
replaymod.gui.editkeyframe.interpolator.linear.desc=Draws straight lines between the keyframes.
|
||||
|
||||
#Render Settings GUI
|
||||
replaymod.gui.rendersettings.title=Rendering Options
|
||||
replaymod.gui.rendersettings.renderer=Rendering Method
|
||||
|
||||
Reference in New Issue
Block a user