Files
ReplayModCinematic/src/main/java/com/replaymod/simplepathing/gui/GuiKeyframeTimeline.java
Jonas Herzig 70acd79431 Allow continued use of most of pathing even if entity tracker fails
The failing entity tracker really only breaks spectator keyframes.
This is also very desirable for minimal mode where entity tracker
failure is highly likely.
2019-06-20 18:44:10 +02:00

329 lines
14 KiB
Java

package com.replaymod.simplepathing.gui;
import com.replaymod.core.ReplayMod;
import com.replaymod.core.versions.MCVer;
import com.replaymod.pathing.properties.CameraProperties;
import com.replaymod.pathing.properties.SpectatorProperty;
import com.replaymod.pathing.properties.TimestampProperty;
import com.replaymod.replay.ReplayModReplay;
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.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;
import org.apache.commons.lang3.tuple.Pair;
import de.johni0702.minecraft.gui.utils.lwjgl.Point;
import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension;
import de.johni0702.minecraft.gui.utils.lwjgl.ReadablePoint;
import java.util.Comparator;
import java.util.Optional;
public class GuiKeyframeTimeline extends AbstractGuiTimeline<GuiKeyframeTimeline> implements Draggable {
protected static final int KEYFRAME_SIZE = 5;
protected static final int KEYFRAME_TEXTURE_X = 74;
protected static final int KEYFRAME_TEXTURE_Y = 20;
private static final int DOUBLE_CLICK_INTERVAL = 250;
private static final int DRAGGING_THRESHOLD = KEYFRAME_SIZE;
private final GuiPathing gui;
/**
* The keyframe (time on timeline) that was last clicked on using the left mouse button.
*/
private long lastClickedKeyframe;
/**
* Path of {@link #lastClickedKeyframe}.
*/
private SPPath lastClickedPath;
/**
* The time at which {@link #lastClickedKeyframe} was updated.
* According to {@link MCVer#milliTime()}.
*/
private long lastClickedTime;
/**
* Whether to handle dragging events.
*/
private boolean dragging;
/**
* Whether we have surpassed the initial threshold and are actually dragging the keyframe.
*/
private boolean actuallyDragging;
/**
* Where the mouse was when {@link #dragging} started.
*/
private int draggingStartX;
/**
* Change caused by dragging. Whenever the user moves the keyframe further, the previous change is undone
* and a new one is created. This way when the mouse is released, only one change is in the undo history.
*/
private Change draggingChange;
public GuiKeyframeTimeline(GuiPathing gui) {
this.gui = gui;
}
@Override
protected void drawTimelineCursor(GuiRenderer renderer, ReadableDimension size) {
ReplayModSimplePathing mod = gui.getMod();
int width = size.getWidth();
int visibleWidth = width - BORDER_LEFT - BORDER_RIGHT;
int startTime = getOffset();
int visibleTime = (int) (getZoom() * getLength());
int endTime = getOffset() + visibleTime;
renderer.bindTexture(ReplayMod.TEXTURE);
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.isSelected(keyframe) ? KEYFRAME_SIZE : 0);
int v = KEYFRAME_TEXTURE_Y;
if (keyframe.getValue(CameraProperties.POSITION).isPresent()) {
if (keyframe.getValue(SpectatorProperty.PROPERTY).isPresent()) {
v += 2 * KEYFRAME_SIZE;
}
renderer.drawTexturedRect(positonX, BORDER_TOP, u, v, KEYFRAME_SIZE, KEYFRAME_SIZE);
}
if (keyframe.getValue(TimestampProperty.PROPERTY).isPresent()) {
v += KEYFRAME_SIZE;
renderer.drawTexturedRect(positonX, BORDER_TOP + KEYFRAME_SIZE, u, v, KEYFRAME_SIZE, KEYFRAME_SIZE);
}
}
});
// Draw colored quads on spectator path segments
for (PathSegment segment : timeline.getPositionPath().getSegments()) {
if (segment.getInterpolator() == null
|| !segment.getInterpolator().getKeyframeProperties().contains(SpectatorProperty.PROPERTY)) {
continue; // Not a spectator segment
}
drawQuadOnSegment(renderer, visibleWidth, segment, BORDER_TOP + 1, 0xFF0088FF);
}
// Draw red quads on time path segments that would require time going backwards
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) {
continue; // All is fine, time is not moving backwards
}
drawQuadOnSegment(renderer, visibleWidth, segment, BORDER_TOP + KEYFRAME_SIZE + 1, 0xFFFF0000);
}
super.drawTimelineCursor(renderer, size);
}
private void drawQuadOnSegment(GuiRenderer renderer, int visibleWidth, PathSegment segment, int y, int color) {
int startTime = getOffset();
int visibleTime = (int) (getZoom() * getLength());
int endTime = getOffset() + visibleTime;
long startFrameTime = segment.getStartKeyframe().getTime();
long endFrameTime = segment.getEndKeyframe().getTime();
if (startFrameTime >= endTime || endFrameTime <= startTime) {
return; // Segment out of display range
}
double relativeStart = startFrameTime - startTime;
double relativeEnd = endFrameTime - startTime;
int startX = BORDER_LEFT + Math.max(0, (int) (relativeStart / visibleTime * visibleWidth) + KEYFRAME_SIZE / 2 + 1);
int endX = BORDER_LEFT + Math.min(visibleWidth, (int) (relativeEnd / visibleTime * visibleWidth) - KEYFRAME_SIZE / 2);
if (startX < endX) {
renderer.drawRect(startX + 1, y, endX - startX - 2, KEYFRAME_SIZE - 2, color);
}
}
/**
* Returns the keyframe at the specified position.
* @param position The raw position
* @return Pair of path id and keyframe or null when no keyframe was clicked
*/
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) {
SPPath path;
if (mouseY <= BORDER_TOP + KEYFRAME_SIZE) {
// Position keyframe
path = SPPath.POSITION;
} else {
// Time keyframe
path = SPPath.TIME;
}
int visibleTime = (int) (getZoom() * getLength());
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.map(Keyframe::getTime).orElse(null));
}
}
return Pair.of(null, null);
}
@Override
public boolean mouseClick(ReadablePoint position, int button) {
int time = getTimeAt(position.getX(), position.getY());
Pair<SPPath, Long> pathKeyframePair = getKeyframe(position);
if (pathKeyframePair.getRight() != null) {
SPPath path = pathKeyframePair.getLeft();
// Clicked on keyframe
long keyframeTime = pathKeyframePair.getRight();
if (button == 0) { // Left click
long now = MCVer.milliTime();
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
gui.openEditKeyframePopup(path, keyframeTime);
return true;
}
}
// Not a double click, just update the click time and selection
lastClickedTime = now;
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);
}
}
return true;
} else if (time != -1) {
// Clicked on timeline but not on any keyframe
if (button == 0) { // Left click
setCursorPosition(time);
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
Path path = gui.getMod().getCurrentTimeline().getPath(pathKeyframePair.getLeft());
path.getKeyframes().stream().flatMap(k -> k.getProperties().stream()).distinct().forEach(
p -> applyPropertyToGame(p, path, time));
}
}
return true;
}
// Missed timeline
return false;
}
// Helper method because generics cannot be defined on blocks
private <T> void applyPropertyToGame(Property<T> property, Path path, long time) {
Optional<T> value = path.getValue(property, time);
if (value.isPresent()) {
property.applyToGame(value.get(), ReplayModReplay.instance.getReplayHandler());
}
}
// Helper method because generics cannot be defined on blocks
private <T> void applyPropertyToGame(Property<T> property, Keyframe keyframe) {
Optional<T> value = keyframe.getValue(property);
if (value.isPresent()) {
property.applyToGame(value.get(), ReplayModReplay.instance.getReplayHandler());
}
}
@Override
public boolean mouseDrag(ReadablePoint position, int button, long timeSinceLastCall) {
if (!dragging) {
if (button == 0) {
// Left click, the user might try to move the cursor by clicking and holding
int time = getTimeAt(position.getX(), position.getY());
if (time != -1) {
// and they are still on the timeline, so update the time appropriately
setCursorPosition(time);
return true;
}
}
return false;
}
if (!actuallyDragging) {
// Check if threshold has been passed by now
if (Math.abs(position.getX() - draggingStartX) >= DRAGGING_THRESHOLD) {
actuallyDragging = true;
}
}
if (actuallyDragging) {
if (!gui.loadEntityTracker(() -> mouseDrag(position, button, timeSinceLastCall))) return true;
// Threshold passed
SPTimeline timeline = gui.getMod().getCurrentTimeline();
Point mouse = new Point(position);
getContainer().convertFor(this, mouse);
int mouseX = mouse.getX();
int width = getLastSize().getWidth();
int bodyWidth = width - BORDER_LEFT - BORDER_RIGHT;
double segmentLength = getLength() * getZoom();
double segmentTime = segmentLength * (mouseX - BORDER_LEFT) / bodyWidth;
int newTime = Math.min(Math.max((int) Math.round(getOffset() + segmentTime), 0), getLength());
if (newTime < 0) {
return true;
}
// If there already is a keyframe at the target time, then increase the time by one until there is none
while (timeline.getKeyframe(lastClickedPath, newTime) != null) {
newTime++;
}
// First undo any previous changes
if (draggingChange != null) {
draggingChange.undo(timeline.getTimeline());
}
// Move keyframe to new position and
// store change for later undoing / pushing to history
draggingChange = timeline.moveKeyframe(lastClickedPath, lastClickedKeyframe, newTime);
// Selected keyframe has been replaced
gui.getMod().setSelected(lastClickedPath, newTime);
}
return true;
}
@Override
public boolean mouseRelease(ReadablePoint position, int button) {
if (dragging) {
if (actuallyDragging) {
gui.getMod().getCurrentTimeline().getTimeline().pushChange(draggingChange);
draggingChange = null;
actuallyDragging = false;
}
dragging = false;
return true;
}
return false;
}
@Override
protected GuiKeyframeTimeline getThis() {
return this;
}
}