Merge branch '1.11' into 1.11.2

0c36022 Merge branch '1.10.2' into 1.11
325d041 Merge branch '1.9.4' into 1.10.2
d9d57e2 Merge branch '1.8.9' into 1.9.4
24c58cb Merge branch '1.8' into 1.8.9
1ffeb01 Update jGui
c122d30 Merge branch '1.8-path-segments' into 1.8
27fd25a Fix crash when server responds with garbage to language query (fixes #51)
db6b0ff Fix / by 0 when two spectator keyframes are closer than 50ms (fixes #60)
d61d0ac Fix hand of invisible spectated player being visible (fixes #59)
f7460f9 Re-enable time path after path playback (fixes #55)
bdbffd5 Add Catmull-Rom spline interpolator to simplepathing
5cbfd28 Generate interpolator settings from InterpolatorType enum
6095aef Fix loading and storing of default interpolator with settings
3f52336 Revert "Fix AbstractMethodError caused by FG2.0 not supporting lambdas (fixes #58)"
c6293bc Update to FG2.2
c8126ed Merge '1.8' into 1.8.9
64898ce Fix NPE when saving recorded resource pack
70e3e54 Fix AbstractMethodError caused by FG2.0 not supporting lambdas (fixes #58)
3f2b3d6 Fix replacing source file in Replay Editor (fixes #57)
d2def94 Fix duration of replays corrupted because of JVM/OS crash
2c64030 Refactor simple pathing timeline logic into dedicated class with unit tests Fix logic for user-set interpolators
4deb374 Enable validation on focus change for all number input fields
5302bca Fix id and text of Replay Editor button in docs
057edcc Manually inject root cert for cross-signed LetsEncrypt cert in requests
cd16211 Register new Property to imply that the path segment following a keyframe has a user-set interpolator instead of the default Modified Keyframe GUI to allow for custom Path interpolator
This commit is contained in:
johni0702
2017-01-29 13:46:06 +01:00
29 changed files with 2017 additions and 427 deletions

View File

@@ -3,6 +3,7 @@ package com.replaymod.core.gui;
import com.google.common.io.Files;
import com.replaymod.replaystudio.PacketData;
import com.replaymod.replaystudio.io.ReplayInputStream;
import com.replaymod.replaystudio.io.ReplayOutputStream;
import com.replaymod.replaystudio.replay.ReplayFile;
import com.replaymod.replaystudio.replay.ReplayMetaData;
import com.replaymod.replaystudio.replay.ZipReplayFile;
@@ -45,15 +46,19 @@ public class RestoreReplayGui extends AbstractGuiScreen<RestoreReplayGui> {
ReplayMetaData metaData = replayFile.getMetaData();
if (metaData != null && metaData.getDuration() == 0) {
// Try to restore replay duration
try (ReplayInputStream in = replayFile.getPacketData()) {
// We need to re-write the packet data in case there are any incomplete packets dangling at the end
try (ReplayInputStream in = replayFile.getPacketData();
ReplayOutputStream out = replayFile.writePacketData()) {
PacketData last = null;
while ((last = in.readPacket()) != null) {
metaData.setDuration((int) last.getTime());
out.write(last);
}
replayFile.writeMetaData(metaData);
} catch (Throwable t) {
t.printStackTrace();
}
// Write back the actual duration
replayFile.writeMetaData(metaData);
}
replayFile.save();
replayFile.close();

View File

@@ -27,8 +27,20 @@ import org.lwjgl.util.ReadableDimension;
import javax.annotation.Nullable;
import javax.imageio.ImageIO;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManagerFactory;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
@@ -51,6 +63,42 @@ public class Utils {
DEFAULT_THUMBNAIL = thumbnail;
}
/**
* Neither the root certificate of LetsEncrypt nor the root that cross-signed it is included in the default
* Java keystore prior to 8u101.
* Therefore whenever a connection to the replaymod.com site is made, this SSLContext has to be used instead.
* It has been constructed to include the necessary root certificates.
* @see #SSL_SOCKET_FACTORY
*/
public static final SSLContext SSL_CONTEXT;
/**
* @see #SSL_CONTEXT
*/
public static final SSLSocketFactory SSL_SOCKET_FACTORY;
static {
// Largely from https://community.letsencrypt.org/t/134/37
try (InputStream in = Utils.class.getResourceAsStream("/dst_root_ca_x3.pem")){
Certificate certificate = CertificateFactory.getInstance("X.509").generateCertificate(in);
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null, null);
keyStore.setCertificateEntry("1", certificate);
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(keyStore);
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(null, trustManagerFactory.getTrustManagers(), null);
SSL_CONTEXT = ctx;
SSL_SOCKET_FACTORY = ctx.getSocketFactory();
} catch (IOException | CertificateException | KeyStoreException | NoSuchAlgorithmException | KeyManagementException e) {
throw new RuntimeException(e);
}
}
public static String convertSecondsToShortString(int seconds) {
int hours = seconds/(60*60);
int min = seconds/60 - hours*60;

View File

@@ -1,5 +1,6 @@
package com.replaymod.editor.gui;
import com.google.common.io.Files;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.gson.JsonObject;
@@ -34,6 +35,7 @@ import de.johni0702.minecraft.gui.popup.GuiInfoPopup;
import de.johni0702.minecraft.gui.popup.GuiYesNoPopup;
import de.johni0702.minecraft.gui.utils.Colors;
import net.minecraft.crash.CrashReport;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.lwjgl.util.ReadableDimension;
@@ -119,10 +121,21 @@ public class GuiReplayEditor extends GuiScreen {
public void save(File inputFile, PacketStream.FilterInfo...filters) {
save(FilenameUtils.getBaseName(inputFile.getName()), (outputFile) -> {
Studio studio = new ReplayStudio();
try (ReplayFile outputReplay = new ZipReplayFile(studio, inputFile, outputFile);
ReplayOutputStream out = outputReplay.writePacketData()) {
// The input replay file MUST be closed before saving the output file
try (ReplayFile inputReplay = new ZipReplayFile(studio, inputFile);
File tmpDir = null;
try {
File actualOutputFile = outputFile;
if (outputFile.getCanonicalPath().equals(inputFile.getCanonicalPath())) {
// Input and output files are identical. Due to the way the ZipReplayFile stores its temporary
// data, the same replay file must not be opened twice for writing (tmp files will be deleted once
// either one is closed or, on Windows, will throw an exception when deleted).
tmpDir = Files.createTempDir();
outputFile = new File(tmpDir, "replay.mcpr");
LOGGER.debug("Output file is identical to input file, using temporary output file {} instead",
outputFile);
}
try (ReplayFile outputReplay = new ZipReplayFile(studio, inputFile, outputFile);
ReplayOutputStream out = outputReplay.writePacketData();
ReplayFile inputReplay = new ZipReplayFile(studio, inputFile);
ReplayInputStream in = inputReplay.getPacketData()) {
ReplayMetaData metaData = inputReplay.getMetaData();
PacketStream stream = studio.createReplayStream(in, true);
@@ -154,11 +167,21 @@ public class GuiReplayEditor extends GuiScreen {
// Update duration of new replay
metaData.setDuration((int) lastTimestamp);
outputReplay.writeMetaData(metaData);
out.close();
outputReplay.save();
}
if (outputFile != actualOutputFile) {
LOGGER.debug("Moving temporary output file {} to {}");
FileUtils.forceDelete(actualOutputFile);
FileUtils.moveFile(outputFile, actualOutputFile);
}
out.close();
outputReplay.save();
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
if (tmpDir != null && !FileUtils.deleteQuietly(tmpDir)) {
LOGGER.warn("Failed to delete temporary directory {}", tmpDir);
}
}
});
}

View File

@@ -39,6 +39,10 @@ import static com.replaymod.editor.ReplayModEditor.LOGGER;
import static java.util.Optional.ofNullable;
public class GuiTrimPanel extends GuiPanel {
private static GuiNumberField newGuiNumberField() {
return new GuiNumberField().setMaxLength(2).setSize(20, 20).setValidateOnFocusChange(true);
}
// Special value indicating no replay files were found
private static final File NO_REPLAY = new File(".");
// Special value for the initial "Select Marker Keyframe" entry
@@ -50,16 +54,16 @@ public class GuiTrimPanel extends GuiPanel {
.setMinSize(new Dimension(200, 20)).onSelection(i -> updateSelectedReplay())
.setToString(f -> f == NO_REPLAY ? "" : FilenameUtils.getBaseName(f.getName()));
public final GuiNumberField startHour = new GuiNumberField().setSize(20, 20).setMaxLength(2);
public final GuiNumberField startMin = new GuiNumberField().setSize(20, 20).setMaxLength(2);
public final GuiNumberField startSec = new GuiNumberField().setSize(20, 20).setMaxLength(2);
public final GuiNumberField startMilli = new GuiNumberField().setSize(40, 20).setMaxLength(4);
public final GuiNumberField startHour = newGuiNumberField();
public final GuiNumberField startMin = newGuiNumberField();
public final GuiNumberField startSec = newGuiNumberField();
public final GuiNumberField startMilli = newGuiNumberField().setSize(40, 20).setMaxLength(4);
public final GuiDropdownMenu<Marker> startMarker = new GuiDropdownMenu<>();
public final GuiNumberField endHour = new GuiNumberField().setSize(20, 20).setMaxLength(2);
public final GuiNumberField endMin = new GuiNumberField().setSize(20, 20).setMaxLength(2);
public final GuiNumberField endSec = new GuiNumberField().setSize(20, 20).setMaxLength(2);
public final GuiNumberField endMilli = new GuiNumberField().setSize(40, 20).setMaxLength(4);
public final GuiNumberField endHour = newGuiNumberField();
public final GuiNumberField endMin = newGuiNumberField();
public final GuiNumberField endSec = newGuiNumberField();
public final GuiNumberField endMilli = newGuiNumberField().setSize(40, 20).setMaxLength(4);
public final GuiDropdownMenu<Marker> endMarker = new GuiDropdownMenu<>();
public final GuiPanel timePanel = new GuiPanel(this)

View File

@@ -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) {

View File

@@ -17,6 +17,7 @@ import de.johni0702.minecraft.gui.utils.Colors;
import net.minecraftforge.fml.common.Loader;
import org.apache.commons.io.FileUtils;
import javax.net.ssl.HttpsURLConnection;
import java.io.File;
import java.io.FileOutputStream;
import java.net.URL;
@@ -24,8 +25,10 @@ import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import static com.replaymod.core.utils.Utils.SSL_SOCKET_FACTORY;
public class OpenEyeExtra implements Extra {
private static final String DOWNLOAD_URL = "http://www.replaymod.com/dl/openeye/" + Loader.MC_VERSION;
private static final String DOWNLOAD_URL = "https://www.replaymod.com/dl/openeye/" + Loader.MC_VERSION;
private static final Setting<Boolean> ASK_FOR_OPEN_EYE = new Setting<>("advanced", "askForOpenEye", null, true);
private ReplayMod mod;
@@ -69,7 +72,9 @@ public class OpenEyeExtra implements Extra {
File targetFile = new File("mods/" + Loader.MC_VERSION, "OpenEye.jar");
FileUtils.forceMkdir(targetFile.getParentFile());
ReadableByteChannel in = Channels.newChannel(new URL(DOWNLOAD_URL).openStream());
HttpsURLConnection connection = (HttpsURLConnection) new URL(DOWNLOAD_URL).openConnection();
connection.setSSLSocketFactory(SSL_SOCKET_FACTORY);
ReadableByteChannel in = Channels.newChannel(connection.getInputStream());
FileChannel out = new FileOutputStream(targetFile).getChannel();
out.transferFrom(in, 0, Long.MAX_VALUE);
} catch (Throwable e) {

View File

@@ -11,7 +11,9 @@ 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.eventhandler.SubscribeEvent;
import org.lwjgl.input.Keyboard;
@@ -92,6 +94,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;
}

View File

@@ -15,13 +15,19 @@ import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import javax.imageio.ImageIO;
import javax.net.ssl.HttpsURLConnection;
import java.awt.image.BufferedImage;
import java.io.*;
import java.net.HttpURLConnection;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.List;
import java.util.function.Consumer;
import static com.replaymod.core.utils.Utils.SSL_SOCKET_FACTORY;
public class ApiClient {
private static final Minecraft mc = Minecraft.getMinecraft();
@@ -130,7 +136,11 @@ public class ApiClient {
QueryBuilder builder = new QueryBuilder(ReplayModApiMethods.get_thumbnail);
builder.put("id", file);
URL url = new URL(builder.toString());
return ImageIO.read(url);
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
connection.setSSLSocketFactory(SSL_SOCKET_FACTORY);
try (InputStream in = connection.getInputStream()) {
return ImageIO.read(in);
}
}
private boolean cancelDownload = false;
@@ -143,7 +153,8 @@ public class ApiClient {
builder.put("id", file);
String url = builder.toString();
URL website = new URL(url);
HttpURLConnection con = (HttpURLConnection) website.openConnection();
HttpsURLConnection con = (HttpsURLConnection) website.openConnection();
con.setSSLSocketFactory(SSL_SOCKET_FACTORY);
int fileSize = con.getContentLength();

View File

@@ -7,12 +7,14 @@ import com.google.gson.JsonParser;
import com.replaymod.online.api.replay.holders.ApiError;
import org.apache.commons.io.IOUtils;
import javax.net.ssl.HttpsURLConnection;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Map;
import static com.replaymod.core.utils.Utils.SSL_SOCKET_FACTORY;
public class SimpleApiClient {
private static final JsonParser jsonParser = new JsonParser();
@@ -72,10 +74,11 @@ public class SimpleApiClient {
// read response
String responseContent = null;
InputStream is = null;
HttpURLConnection httpUrlConnection = null;
HttpsURLConnection httpUrlConnection = null;
try {
URL url = new URL(urlString);
httpUrlConnection = (HttpURLConnection) url.openConnection();
httpUrlConnection = (HttpsURLConnection) url.openConnection();
httpUrlConnection.setSSLSocketFactory(SSL_SOCKET_FACTORY);
httpUrlConnection.setRequestMethod("GET");

View File

@@ -9,16 +9,18 @@ import lombok.RequiredArgsConstructor;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import javax.net.ssl.HttpsURLConnection;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.util.Set;
import java.util.function.Consumer;
import static com.replaymod.core.utils.Utils.SSL_SOCKET_FACTORY;
@RequiredArgsConstructor
public class FileUploader {
private static final Gson gson = new Gson();
@@ -48,7 +50,8 @@ public class FileUploader {
postData += "&name=" + URLEncoder.encode(filename, "UTF-8");
String url = ReplayModApiMethods.upload_file + postData;
HttpURLConnection con = (HttpURLConnection) new URL(url).openConnection();
HttpsURLConnection con = (HttpsURLConnection) new URL(url).openConnection();
con.setSSLSocketFactory(SSL_SOCKET_FACTORY);
con.setUseCaches(false);
con.setDoOutput(true);
con.setRequestMethod("POST");

View File

@@ -2,7 +2,7 @@ package com.replaymod.online.api.replay;
public class ReplayModApiMethods {
public static final String REPLAYMOD_BASE_URL = "http://ReplayMod.com/api/";
public static final String REPLAYMOD_BASE_URL = "https://ReplayMod.com/api/";
public static final String register = REPLAYMOD_BASE_URL+"register";
public static final String check_authkey = REPLAYMOD_BASE_URL+"check_authkey";

View File

@@ -26,6 +26,7 @@ import javax.annotation.Nonnull;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;
/**
@@ -52,6 +53,9 @@ public class ResourcePackRecorder {
boolean doWrite = false; // Whether we are the first and have to write it
synchronized (replayFile) { // Need to read, modify and write the resource pack index atomically
Map<Integer, String> index = replayFile.getResourcePackIndex();
if (index == null) {
index = new HashMap<>();
}
if (!index.containsValue(hash)) {
// Hash is unknown, we have to write the resource pack ourselves
doWrite = true;

View File

@@ -80,8 +80,8 @@ public class GuiRenderSettings extends GuiScreen implements Closeable {
}
}).setMinSize(new Dimension(0, 20)).setValues(RenderSettings.EncodingPreset.values());
public final GuiNumberField videoWidth = new GuiNumberField().setSize(50, 20).setMinValue(1);
public final GuiNumberField videoHeight = new GuiNumberField().setSize(50, 20).setMinValue(1);
public final GuiNumberField videoWidth = new GuiNumberField().setSize(50, 20).setMinValue(1).setValidateOnFocusChange(true);
public final GuiNumberField videoHeight = new GuiNumberField().setSize(50, 20).setMinValue(1).setValidateOnFocusChange(true);
public final GuiSlider frameRateSlider = new GuiSlider().onValueChanged(new Runnable() {
@Override
public void run() {
@@ -93,7 +93,7 @@ public class GuiRenderSettings extends GuiScreen implements Closeable {
.setLayout(new HorizontalLayout(HorizontalLayout.Alignment.RIGHT).setSpacing(2))
.addElements(new HorizontalLayout.Data(0.5), videoWidth, new GuiLabel().setText("*"), videoHeight);
public final GuiNumberField bitRateField = new GuiNumberField().setValue(10).setSize(50, 20);
public final GuiNumberField bitRateField = new GuiNumberField().setValue(10).setSize(50, 20).setValidateOnFocusChange(true);
public final GuiDropdownMenu<String> bitRateUnit = new GuiDropdownMenu<String>()
.setSize(50, 20).setValues("bps", "kbps", "mbps").setSelected("mbps");

View File

@@ -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();

View File

@@ -16,6 +16,10 @@ import org.lwjgl.input.Keyboard;
import org.lwjgl.util.ReadablePoint;
public class GuiEditMarkerPopup extends AbstractGuiPopup<GuiEditMarkerPopup> implements Typeable {
private static GuiNumberField newGuiNumberField() {
return new GuiNumberField().setSize(150, 20).setValidateOnFocusChange(true);
}
private final ReplayHandler replayHandler;
private final Marker marker;
@@ -23,15 +27,15 @@ public class GuiEditMarkerPopup extends AbstractGuiPopup<GuiEditMarkerPopup> imp
public final GuiTextField nameField = new GuiTextField().setSize(150, 20);
// TODO: Replace with a min/sec/msec field
public final GuiNumberField timeField = new GuiNumberField().setSize(150, 20).setPrecision(0);
public final GuiNumberField timeField = newGuiNumberField().setPrecision(0);
public final GuiNumberField xField = new GuiNumberField().setSize(150, 20).setPrecision(10);
public final GuiNumberField yField = new GuiNumberField().setSize(150, 20).setPrecision(10);
public final GuiNumberField zField = new GuiNumberField().setSize(150, 20).setPrecision(10);
public final GuiNumberField xField = newGuiNumberField().setPrecision(10);
public final GuiNumberField yField = newGuiNumberField().setPrecision(10);
public final GuiNumberField zField = newGuiNumberField().setPrecision(10);
public final GuiNumberField yawField = new GuiNumberField().setSize(150, 20).setPrecision(5);
public final GuiNumberField pitchField = new GuiNumberField().setSize(150, 20).setPrecision(5);
public final GuiNumberField rollField = new GuiNumberField().setSize(150, 20).setPrecision(5);
public final GuiNumberField yawField = newGuiNumberField().setPrecision(5);
public final GuiNumberField pitchField = newGuiNumberField().setPrecision(5);
public final GuiNumberField rollField = newGuiNumberField().setPrecision(5);
public final GuiPanel inputs = GuiPanel.builder()
.layout(new GridLayout().setColumns(2).setSpacingX(7).setSpacingY(3))

View File

@@ -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();
}
}

View File

@@ -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.common.MinecraftForge;
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() {

View 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.setDetail("Interpolator", interpolator::toString);
throw new ReportedException(crash);
}
return baos.toString();
}
private Interpolator deserializeInterpolator(String json) {
JsonReader jsonReader = new JsonReader(new StringReader(json));
try {
jsonReader.beginArray();
return deserializeInterpolator(jsonReader);
} catch (IOException e) {
CrashReport crash = CrashReport.makeCrashReport(e, "De-serializing interpolator");
CrashReportCategory category = crash.makeCategory("De-serializing interpolator");
category.addCrashSection("Interpolator", json);
throw new ReportedException(crash);
}
}
}

View File

@@ -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);

View File

@@ -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;
}
}
}
}
}

View File

@@ -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;
}

View File

@@ -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();
}
}

View File

@@ -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;
}

View File

@@ -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;
}
}