diff --git a/.drone.yml b/.drone.yml
index 3ea70281..6c4afb43 100644
--- a/.drone.yml
+++ b/.drone.yml
@@ -7,7 +7,6 @@ pipeline:
commands:
- curl --fail --create-dirs -o $HOME/.gradle/init.d/mirror.gradle https://maven.johni0702.de/config/gradle
- curl --fail --create-dirs -o $HOME/.m2/settings.xml https://maven.johni0702.de/config/maven
- - JAVA_OPTS=-Xmx1516M ./gradlew setupDecompWorkspace
- ./gradlew :build
archive:
image: plugins/s3
diff --git a/.drone.yml.sig b/.drone.yml.sig
index 95a141a6..9cdc53b7 100644
--- a/.drone.yml.sig
+++ b/.drone.yml.sig
@@ -1 +1 @@
-eyJhbGciOiJIUzI1NiJ9.cGlwZWxpbmU6CiAgY2xvbmU6CiAgICBpbWFnZTogcGx1Z2lucy9naXQKICAgIHRhZ3M6IHRydWUKICBidWlsZDoKICAgIGltYWdlOiBtYXZlbjozLjMuOS1qZGstOAogICAgY29tbWFuZHM6CiAgICAgIC0gY3VybCAtLWZhaWwgLS1jcmVhdGUtZGlycyAtbyAkSE9NRS8uZ3JhZGxlL2luaXQuZC9taXJyb3IuZ3JhZGxlIGh0dHBzOi8vbWF2ZW4uam9obmkwNzAyLmRlL2NvbmZpZy9ncmFkbGUKICAgICAgLSBjdXJsIC0tZmFpbCAtLWNyZWF0ZS1kaXJzIC1vICRIT01FLy5tMi9zZXR0aW5ncy54bWwgaHR0cHM6Ly9tYXZlbi5qb2huaTA3MDIuZGUvY29uZmlnL21hdmVuCiAgICAgIC0gSkFWQV9PUFRTPS1YbXgxNTE2TSAuL2dyYWRsZXcgc2V0dXBEZWNvbXBXb3Jrc3BhY2UKICAgICAgLSAuL2dyYWRsZXcgOmJ1aWxkCiAgYXJjaGl2ZToKICAgIGltYWdlOiBwbHVnaW5zL3MzCiAgICBhY2w6IHB1YmxpYy1yZWFkCiAgICByZWdpb246ICJ1cy1lYXN0LTEiCiAgICBidWNrZXQ6ICJyZXBsYXltb2QiCiAgICBwYXRoX3N0eWxlOiB0cnVlCiAgICBzb3VyY2U6IGJ1aWxkL2xpYnMvKgogICAgc3RyaXBfcHJlZml4OiBidWlsZC9saWJzLwogICAgdGFyZ2V0OiAvCg.nzPW0_TpMIjnFZZ5Ci439QDKlaDo1a3DCH-VCYUgpx8
\ No newline at end of file
+eyJhbGciOiJIUzI1NiJ9.cGlwZWxpbmU6CiAgY2xvbmU6CiAgICBpbWFnZTogcGx1Z2lucy9naXQKICAgIHRhZ3M6IHRydWUKICBidWlsZDoKICAgIGltYWdlOiBtYXZlbjozLjMuOS1qZGstOAogICAgY29tbWFuZHM6CiAgICAgIC0gY3VybCAtLWZhaWwgLS1jcmVhdGUtZGlycyAtbyAkSE9NRS8uZ3JhZGxlL2luaXQuZC9taXJyb3IuZ3JhZGxlIGh0dHBzOi8vbWF2ZW4uam9obmkwNzAyLmRlL2NvbmZpZy9ncmFkbGUKICAgICAgLSBjdXJsIC0tZmFpbCAtLWNyZWF0ZS1kaXJzIC1vICRIT01FLy5tMi9zZXR0aW5ncy54bWwgaHR0cHM6Ly9tYXZlbi5qb2huaTA3MDIuZGUvY29uZmlnL21hdmVuCiAgICAgIC0gLi9ncmFkbGV3IDpidWlsZAogIGFyY2hpdmU6CiAgICBpbWFnZTogcGx1Z2lucy9zMwogICAgYWNsOiBwdWJsaWMtcmVhZAogICAgcmVnaW9uOiAidXMtZWFzdC0xIgogICAgYnVja2V0OiAicmVwbGF5bW9kIgogICAgcGF0aF9zdHlsZTogdHJ1ZQogICAgc291cmNlOiBidWlsZC9saWJzLyoKICAgIHN0cmlwX3ByZWZpeDogYnVpbGQvbGlicy8KICAgIHRhcmdldDogLwo.Z6rpaLplJKQ4pzB8AjCnplgci42qEu7L4LeS0fGm4SY
\ No newline at end of file
diff --git a/.gitmodules b/.gitmodules
index eef9434a..ea042fed 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -5,3 +5,7 @@
[submodule "ReplayStudio"]
path = ReplayStudio
url = https://github.com/ReplayMod/ReplayStudio
+
+[submodule "src/main/resources/assets/replaymod/lang"]
+ path = src/main/resources/assets/replaymod/lang
+ url = https://github.com/ReplayMod/Translations
diff --git a/ReplayStudio b/ReplayStudio
index f778f556..d066d7e2 160000
--- a/ReplayStudio
+++ b/ReplayStudio
@@ -1 +1 @@
-Subproject commit f778f556a7f5317d6ed2d66f7f9e410c72b1d225
+Subproject commit d066d7e210b5c24778f945c9882e027e50d278a4
diff --git a/build.gradle b/build.gradle
index 451925e2..6d51a6a4 100755
--- a/build.gradle
+++ b/build.gradle
@@ -1,7 +1,5 @@
import groovy.json.JsonOutput
-import java.util.zip.ZipInputStream
-
buildscript {
repositories {
mavenCentral()
@@ -61,7 +59,7 @@ configurations {
dependencies {
compile 'org.projectlombok:lombok:1.16.4'
- compile 'org.spongepowered:mixin:0.6.4-SNAPSHOT'
+ compile 'org.spongepowered:mixin:0.6.8-SNAPSHOT'
shade 'com.googlecode.mp4parser:isoparser:1.1.7'
shade 'org.apache.commons:commons-exec:1.3'
shade 'com.google.apis:google-api-services-youtube:v3-rev178-1.22.0'
@@ -103,27 +101,6 @@ jar {
}
from noticeDir
- def langDir = file("$buildDir/languages")
- doFirst {
- try {
- langDir.deleteDir()
- langDir.mkdirs()
- def dir = new File(langDir, 'assets/replaymod/lang/')
- dir.mkdirs()
- def zip = new ZipInputStream(new URL('http://replaymod.com/api/grab_languages').openStream())
- def e;
- while ((e = zip.nextEntry) != null) {
- new File(dir, e.getName()) << zip
- zip.closeEntry()
- }
- } catch(Exception e) {
- e.printStackTrace();
- }
- }
- from (langDir) {
- exclude '**/en_US.lang'
- }
-
from ({shade().collect { it.isDirectory() ? it : zipTree(it) }}) {
exclude '**/NOTICE*'
// exclude everything taken in from jGui for running the mod in a dev environment
@@ -189,6 +166,13 @@ sourceSets {
}
refMap = "mixins.replaymod.refmap.json"
}
+ integrationTest {
+ compileClasspath += main.runtimeClasspath + main.output
+ java {
+ srcDir file('src/integration-test/java')
+ }
+ resources.srcDir file('src/integration-test/resources')
+ }
}
task copySrg(type: Copy, dependsOn: 'genSrgs') {
@@ -200,6 +184,81 @@ setupDecompWorkspace.dependsOn copySrg
setupDevWorkspace.dependsOn copySrg
project.tasks.idea.dependsOn copySrg
+
+import org.objectweb.asm.*
+
+import java.util.zip.ZipEntry
+import java.util.zip.ZipFile
+import java.util.zip.ZipOutputStream
+
+import static org.objectweb.asm.Opcodes.ASM5
+
+// MC binaries were complied with a java version that produces invalid class files under certain circumstances
+// This causes setupCIWorkspace to be insufficient for compiling.
+// Related JDK bug: https://bugs.openjdk.java.net/browse/JDK-8066725
+// As a workaround, to use setupCIWorkspace on Drone, we modify the bin jar in-place and remove all parameter annotations.
+// WARNING: This piece of code ignores any and all gradle conventions and will probably fail horribly when run outside
+// of a single-use environment (e.g. Drone). Use setupDecompWorkspace for normal use.
+tasks.deobfMcMCP.doLast {
+ println "Applying RuntimeInvisibleParameterAnnotations workaround..."
+ File jar = getOutJar()
+ File tmp = new File((File) getTemporaryDir(), "workaround.jar")
+ tmp.withOutputStream {
+ new ZipOutputStream(it).withStream { dst ->
+ new ZipFile(jar).withCloseable { src ->
+ src.entries().each {
+ if (it.name.startsWith("net/minecraft/") && it.name.endsWith(".class")) {
+ def cw = new ClassWriter(0)
+ def cv = new ClassVisitor(ASM5, cw) {
+ @Override
+ MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
+ return new MethodVisitor(ASM5, cv.visitMethod(access, name, desc, signature, exceptions)) {
+ @Override
+ AnnotationVisitor visitParameterAnnotation(int parameter, String pdesc, boolean visible) {
+ return null // Strip all parameter annotations
+ }
+ }
+ }
+ }
+ new ClassReader(src.getInputStream(it)).accept(cv, 0)
+ dst.putNextEntry(new ZipEntry(it.name))
+ dst.write(cw.toByteArray())
+ } else {
+ dst.putNextEntry(it)
+ dst.write(src.getInputStream(it).bytes)
+ }
+ }
+ }
+ }
+ }
+ jar.delete()
+ tmp.renameTo(jar)
+}
+
+task runIntegrationTest(type: JavaExec, dependsOn: ["makeStart", "jar"]) {
+ main = 'GradleStart'
+ standardOutput = System.out
+ errorOutput = System.err
+ workingDir file(minecraft.runDir)
+
+ def testDir = new File(minecraft.runDir, "integration-test")
+ doFirst {
+ testDir.deleteDir()
+ testDir.mkdirs()
+ }
+
+ doLast {
+ testDir.deleteDir()
+ }
+
+ afterEvaluate {
+ def runClient = tasks.getByName("runClient")
+ runIntegrationTest.jvmArgs = runClient.jvmArgs + "-Dfml.noGrab=true"
+ runIntegrationTest.args = runClient.args + "--gameDir" + testDir.canonicalPath
+ runIntegrationTest.classpath runClient.classpath + sourceSets.integrationTest.output
+ }
+}
+
def generateVersionsJson() {
// List all tags
def stdout = new ByteArrayOutputStream()
diff --git a/jGui b/jGui
index 97566db8..bd3fa93d 160000
--- a/jGui
+++ b/jGui
@@ -1 +1 @@
-Subproject commit 97566db8eda598f70317424ddf47a088956c1b8f
+Subproject commit bd3fa93d7d531792c97958795eddd27438d1635a
diff --git a/src/integration-test/java/IntegrationTest.java b/src/integration-test/java/IntegrationTest.java
new file mode 100644
index 00000000..6f39680e
--- /dev/null
+++ b/src/integration-test/java/IntegrationTest.java
@@ -0,0 +1,30 @@
+import org.apache.commons.io.FileUtils;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Entry point for running integration tests from within the IDE.
+ * This is not called when running integration tests directly from gradle.
+ */
+public class IntegrationTest {
+ public static void main(String[] args) throws Throwable {
+ // Prevent MC from grabbing our mouse during the test (it's not needed)
+ System.setProperty("fml.noGrab", "true");
+
+ // Make sure the test folder exists and is fresh
+ File gameDir = new File(System.getProperty("user.dir"), "integration-test");
+ if (gameDir.exists()) {
+ FileUtils.forceDelete(gameDir);
+ }
+ FileUtils.forceMkdir(gameDir);
+
+ // Set game dir to test folder and call regular entry point
+ List argsList = new ArrayList<>(Arrays.asList(args));
+ argsList.add("--gameDir");
+ argsList.add(gameDir.getCanonicalPath());
+ GradleStart.main(argsList.toArray(new String[argsList.size()]));
+ }
+}
diff --git a/src/integration-test/java/com/replaymod/core/AbstractTask.java b/src/integration-test/java/com/replaymod/core/AbstractTask.java
new file mode 100644
index 00000000..c6abf8f5
--- /dev/null
+++ b/src/integration-test/java/com/replaymod/core/AbstractTask.java
@@ -0,0 +1,340 @@
+package com.replaymod.core;
+
+import com.google.common.collect.Iterables;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.SettableFuture;
+import de.johni0702.minecraft.gui.container.AbstractGuiOverlay;
+import de.johni0702.minecraft.gui.container.AbstractGuiScreen;
+import de.johni0702.minecraft.gui.container.GuiContainer;
+import de.johni0702.minecraft.gui.container.GuiOverlay;
+import de.johni0702.minecraft.gui.container.GuiScreen;
+import de.johni0702.minecraft.gui.element.GuiButton;
+import de.johni0702.minecraft.gui.element.GuiElement;
+import de.johni0702.minecraft.gui.element.GuiTexturedButton;
+import de.johni0702.minecraft.gui.popup.AbstractGuiPopup;
+import de.johni0702.minecraft.gui.utils.Consumer;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.settings.KeyBinding;
+import net.minecraftforge.common.MinecraftForge;
+import net.minecraftforge.fml.common.FMLCommonHandler;
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
+import net.minecraftforge.fml.common.gameevent.TickEvent;
+import org.lwjgl.input.Keyboard;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.concurrent.TimeoutException;
+
+import static com.replaymod.core.ReplayModIntegrationTest.LOGGER;
+import static com.replaymod.core.utils.Utils.addCallback;
+
+public abstract class AbstractTask implements Task {
+ public static Task create(Consumer init) {
+ return new AbstractTask() {
+ @Override
+ protected void init() {
+ init.consume(this);
+ }
+ };
+ }
+
+ public static final Minecraft mc = Minecraft.getMinecraft();
+ public final ReplayMod core = ReplayMod.instance;
+ public SettableFuture future;
+
+ @Override
+ public ListenableFuture execute() {
+ future = SettableFuture.create();
+
+ MinecraftForge.EVENT_BUS.register(this);
+ addCallback(future, success -> {
+ MinecraftForge.EVENT_BUS.unregister(this);
+ }, error -> {});
+
+ init();
+
+ return future;
+ }
+
+ protected void init() {}
+
+ protected void runLater(Runnable runnable) {
+ core.runLater(() -> {
+ try {
+ runnable.run();
+ } catch (Throwable t) {
+ future.setException(t);
+ }
+ });
+ }
+
+ public void expectGuiClosed(Runnable onClosed) {
+ expectGuiClosed0(10, onClosed);
+ }
+
+ public void expectGuiClosed(int timeout, Runnable onClosed) {
+ expectGuiClosed0(timeout, onClosed);
+ }
+
+ private void expectGuiClosed0(int timeout, Runnable onClosed) {
+ StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
+ class EventHandler {
+ final net.minecraft.client.gui.GuiScreen currentScreen = mc.currentScreen;
+ int framesPassed;
+
+ @SubscribeEvent
+ public void onGuiOpen(TickEvent.RenderTickEvent event) {
+ if (event.phase != TickEvent.Phase.START) return;
+ if (currentScreen != mc.currentScreen) {
+ MinecraftForge.EVENT_BUS.unregister(this);
+ onClosed.run();
+ } else {
+ if (framesPassed < timeout) {
+ framesPassed++;
+ } else {
+ Object gui = (gui = GuiScreen.from(currentScreen)) == null ? currentScreen : gui;
+ Exception e = new TimeoutException("Timeout while waiting for " + gui + " to be closed.");
+ e.setStackTrace(Arrays.copyOfRange(stackTrace, 3, stackTrace.length));
+ future.setException(e);
+ }
+ }
+ }
+ }
+ MinecraftForge.EVENT_BUS.register(new EventHandler());
+ }
+
+ public void expectPopupClosed(Runnable onClosed) {
+ expectPopupClosed0(10, onClosed);
+ }
+
+ public void expectPopupClosed(int timeout, Runnable onClosed) {
+ expectPopupClosed0(timeout, onClosed);
+ }
+
+ private void expectPopupClosed0(int timeout, Runnable onClosed) {
+ StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
+ AbstractGuiPopup popup = getPopup(mc.currentScreen);
+ if (popup == null) {
+ throw new IllegalStateException("No popup found.");
+ }
+ class EventHandler {
+ int framesPassed;
+
+ @SubscribeEvent
+ public void onGuiOpen(TickEvent.RenderTickEvent event) {
+ if (event.phase != TickEvent.Phase.START) return;
+ if (getPopup(mc.currentScreen) != popup) {
+ MinecraftForge.EVENT_BUS.unregister(this);
+ onClosed.run();
+ } else {
+ if (framesPassed < timeout) {
+ framesPassed++;
+ } else {
+ Exception e = new TimeoutException("Timeout while waiting for " + popup + " to be closed.");
+ e.setStackTrace(Arrays.copyOfRange(stackTrace, 3, stackTrace.length));
+ future.setException(e);
+ }
+ }
+ }
+ }
+ MinecraftForge.EVENT_BUS.register(new EventHandler());
+ }
+
+ private AbstractGuiPopup getPopup(net.minecraft.client.gui.GuiScreen minecraft) {
+ GuiContainer> container = GuiOverlay.from(minecraft);
+ if (container == null) {
+ container = GuiScreen.from(minecraft);
+ }
+ if (container != null) {
+ while (container.getContainer() != null) {
+ container = container.getContainer();
+ }
+ GuiElement popup = Iterables.getLast(container.getChildren());
+ if (popup instanceof AbstractGuiPopup) {
+ return (AbstractGuiPopup) popup;
+ }
+ }
+ return null;
+ }
+
+ public void expectGui(Class guiClass, Consumer onOpen) {
+ StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
+ class EventHandler {
+ net.minecraft.client.gui.GuiScreen currentScreen;
+ int framesPassed;
+
+ @SubscribeEvent
+ public void onGuiOpen(TickEvent.RenderTickEvent event) {
+ if (event.phase != TickEvent.Phase.START) return;
+ if (currentScreen != mc.currentScreen) {
+ currentScreen = mc.currentScreen;
+ framesPassed = 0;
+ }
+ if (framesPassed < 10) {
+ framesPassed++;
+ return;
+ }
+
+ MinecraftForge.EVENT_BUS.unregister(this);
+
+ Object foundGui = null;
+ if (AbstractGuiScreen.class.isAssignableFrom(guiClass)) {
+ AbstractGuiScreen guiScreen = GuiScreen.from(currentScreen);
+ if (guiClass.isInstance(guiScreen)) {
+ onOpen.consume(guiClass.cast(guiScreen));
+ return;
+ }
+ foundGui = guiScreen;
+ } else if (AbstractGuiOverlay.class.isAssignableFrom(guiClass)) {
+ AbstractGuiOverlay guiScreen = GuiOverlay.from(currentScreen);
+ if (guiClass.isInstance(guiScreen)) {
+ onOpen.consume(guiClass.cast(guiScreen));
+ return;
+ }
+ foundGui = guiScreen;
+ } else if (AbstractGuiPopup.class.isAssignableFrom(guiClass)) {
+ AbstractGuiPopup popup = getPopup(currentScreen);
+ if (guiClass.isInstance(popup)) {
+ onOpen.consume(guiClass.cast(popup));
+ return;
+ }
+ } else {
+ if (guiClass.isInstance(currentScreen)) {
+ onOpen.consume(guiClass.cast(currentScreen));
+ return;
+ }
+ }
+ class UnexpectedGuiException extends Exception {
+ UnexpectedGuiException(Object foundGui) {
+ super("Expected instance of " + guiClass + " but found " + foundGui);
+ setStackTrace(Arrays.copyOfRange(stackTrace, 2, stackTrace.length));
+ }
+ }
+ future.setException(new UnexpectedGuiException(foundGui == null ? currentScreen : foundGui));
+ }
+ }
+ MinecraftForge.EVENT_BUS.register(new EventHandler());
+ }
+
+ private void clickNow(int x, int y) {
+ try {
+ Method method = net.minecraft.client.gui.GuiScreen.class
+ .getDeclaredMethod("mouseClicked", int.class, int.class, int.class);
+ method.setAccessible(true);
+ method.invoke(mc.currentScreen, x, y, 0);
+ } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
+ future.setException(e);
+ }
+ }
+
+ public void click(int x, int y) {
+ runLater(() -> {
+ LOGGER.info("Clicking at {}/{}", x, y);
+ clickNow(x, y);
+ });
+ }
+
+ private void dragNow(int x, int y) {
+ try {
+ Method method = net.minecraft.client.gui.GuiScreen.class
+ .getDeclaredMethod("mouseClickMove", int.class, int.class, int.class, long.class);
+ method.setAccessible(true);
+ method.invoke(mc.currentScreen, x, y, 0, 0);
+ } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
+ future.setException(e);
+ }
+ }
+
+ public void drag(int x, int y) {
+ runLater(() -> {
+ LOGGER.info("Dragging to {}/{}", x, y);
+ dragNow(x, y);
+ });
+ }
+
+ public void click(GuiButton button) {
+ runLater(() -> {
+ if (!button.isEnabled()) {
+ future.setException(new IllegalStateException("Button is disabled: " + button.getLabel()));
+ return;
+ }
+ LOGGER.info("Clicking button {}", button.getLabel());
+ button.onClick();
+ });
+ }
+
+ public void click(GuiTexturedButton button) {
+ runLater(() -> {
+ if (!button.isEnabled()) {
+ future.setException(new IllegalStateException("Button is disabled: " + button.getTexture()));
+ return;
+ }
+ LOGGER.info("Clicking textured button {}", button.getTexture());
+ button.onClick();
+ });
+ }
+
+ public void click(String buttonText) {
+ runLater(() -> {
+ LOGGER.info("Clicking button {}", buttonText);
+ try {
+ Field field = net.minecraft.client.gui.GuiScreen.class.getDeclaredField("buttonList");
+ field.setAccessible(true);
+ @SuppressWarnings("unchecked")
+ List buttonList = (List)
+ field.get(mc.currentScreen);
+
+ net.minecraft.client.gui.GuiButton button = null;
+ for (net.minecraft.client.gui.GuiButton guiButton : buttonList) {
+ if (guiButton.displayString.equals(buttonText)) {
+ button = guiButton;
+ }
+ }
+ if (button == null) {
+ future.setException(new NoSuchElementException("No button with label: " + buttonText));
+ return;
+ }
+
+ clickNow(button.xPosition + 5, button.yPosition + 5);
+ } catch (IllegalAccessException | NoSuchFieldException e) {
+ future.setException(e);
+ }
+ });
+ }
+
+ public void type(String string) {
+ for (char c : string.toCharArray()) {
+ press(c, Keyboard.getKeyIndex(String.valueOf(c).toUpperCase()));
+ }
+ }
+
+ public void press(int keyCode) {
+ String keyName = Keyboard.getKeyName(keyCode);
+ char character = keyName.length() == 1 ? keyName.charAt(0) : '\0';
+ press(character, keyCode);
+ }
+
+ public void press(char character, int keyCode) {
+ runLater(() -> {
+ LOGGER.info("Pressing key {}", Keyboard.getKeyName(keyCode));
+ if (mc.currentScreen == null || mc.currentScreen.allowUserInput) {
+ KeyBinding.onTick(keyCode);
+ FMLCommonHandler.instance().fireKeyInput();
+ }
+ if (mc.currentScreen != null) {
+ try {
+ Method method = net.minecraft.client.gui.GuiScreen.class
+ .getDeclaredMethod("keyTyped", char.class, int.class);
+ method.setAccessible(true);
+ method.invoke(mc.currentScreen, character, keyCode);
+ } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
+ future.setException(e);
+ }
+ }
+ });
+ }
+}
diff --git a/src/integration-test/java/com/replaymod/core/CompositeTask.java b/src/integration-test/java/com/replaymod/core/CompositeTask.java
new file mode 100644
index 00000000..deeb2529
--- /dev/null
+++ b/src/integration-test/java/com/replaymod/core/CompositeTask.java
@@ -0,0 +1,41 @@
+package com.replaymod.core;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.SettableFuture;
+
+import static com.replaymod.core.ReplayModIntegrationTest.LOGGER;
+import static com.replaymod.core.utils.Utils.addCallback;
+
+public class CompositeTask implements Task {
+ private SettableFuture future;
+ protected final Task[] children;
+
+ public CompositeTask(Task[] children) {
+ this.children = children;
+ }
+
+ @Override
+ public ListenableFuture execute() {
+ future = SettableFuture.create();
+ executeChild(0);
+ return future;
+ }
+
+ private void executeChild(int childIndex) {
+ if (future.isDone()) return;
+ if (childIndex < children.length) {
+ ReplayMod.instance.runLater(() -> {
+ try {
+ Task task = children[childIndex];
+ LOGGER.info("Running task {}", task);
+ ListenableFuture childFuture = task.execute();
+ addCallback(childFuture, done -> executeChild(childIndex + 1), err -> future.setException(err));
+ } catch (Throwable t) {
+ future.setException(t);
+ }
+ });
+ } else {
+ future.set(null);
+ }
+ }
+}
diff --git a/src/integration-test/java/com/replaymod/core/ReplayModIntegrationTest.java b/src/integration-test/java/com/replaymod/core/ReplayModIntegrationTest.java
new file mode 100644
index 00000000..3fc0d00f
--- /dev/null
+++ b/src/integration-test/java/com/replaymod/core/ReplayModIntegrationTest.java
@@ -0,0 +1,73 @@
+package com.replaymod.core;
+
+import com.replaymod.core.regression.RegressionTest60;
+import com.replaymod.core.regression.RegressionTest61;
+import com.replaymod.core.regression.RegressionTest62;
+import com.replaymod.extra.DownloadOpenEye;
+import com.replaymod.online.SkipLogin;
+import com.replaymod.recording.CreateSPWorld;
+import com.replaymod.recording.ExitSPWorld;
+import com.replaymod.replay.ExitReplay;
+import com.replaymod.replay.LoadReplay;
+import com.replaymod.replay.OpenReplayViewer;
+import net.minecraft.client.Minecraft;
+import net.minecraftforge.fml.common.FMLCommonHandler;
+import net.minecraftforge.fml.common.Mod;
+import net.minecraftforge.fml.common.event.FMLPreInitializationEvent;
+import org.apache.logging.log4j.Logger;
+
+import static com.replaymod.core.AbstractTask.mc;
+import static com.replaymod.core.ReplayModIntegrationTest.MOD_ID;
+import static com.replaymod.core.utils.Utils.addCallback;
+
+/**
+ * Helper mod that initiates the integration tests.
+ */
+@Mod(modid = MOD_ID)
+public class ReplayModIntegrationTest {
+ public static final String MOD_ID = "replaymod-integration-test";
+
+ public static Logger LOGGER;
+
+ @Mod.EventHandler
+ public void init(FMLPreInitializationEvent event) {
+ LOGGER = event.getModLog();
+
+ // Make sure the game window doesn't have to remain in focus during the test
+ mc.gameSettings.pauseOnLostFocus = false;
+
+ runTasks(
+ new SkipLogin(),
+ new DownloadOpenEye(),
+ new CreateSPWorld(),
+ new Wait(5000),
+ new ExitSPWorld(),
+ new OpenReplayViewer(),
+ new LoadReplay(),
+
+ new RegressionTest60(),
+ new RegressionTest61(),
+ new RegressionTest62(),
+
+ // new AbstractTask() {}, // Uncomment to not exit on success (useful for writing more tests)
+ new ExitReplay()
+ );
+ }
+
+ private void runTasks(Task... tests) {
+ addCallback(new CompositeTask(tests).execute(), success -> {
+ if (!Minecraft.getMinecraft().hasCrashed) {
+ LOGGER.info("===================================================");
+ LOGGER.info("= ALL TESTS PASSED =");
+ LOGGER.info("===================================================");
+ FMLCommonHandler.instance().exitJava(0, false);
+ }
+ }, error -> {
+ LOGGER.error("Failed task:", error);
+ LOGGER.error("===================================================");
+ LOGGER.error("= TEST FAILED =");
+ LOGGER.error("===================================================");
+ FMLCommonHandler.instance().exitJava(1, false);
+ });
+ }
+}
diff --git a/src/integration-test/java/com/replaymod/core/Task.java b/src/integration-test/java/com/replaymod/core/Task.java
new file mode 100644
index 00000000..4db75a0e
--- /dev/null
+++ b/src/integration-test/java/com/replaymod/core/Task.java
@@ -0,0 +1,35 @@
+package com.replaymod.core;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+public interface Task {
+ ListenableFuture execute();
+
+ static Task click(int x, int y) {
+ return AbstractTask.create(task -> {
+ task.click(x, y);
+ task.runLater(() -> task.future.set(null));
+ });
+ }
+
+ static Task drag(int x, int y) {
+ return AbstractTask.create(task -> {
+ task.drag(x, y);
+ task.runLater(() -> task.future.set(null));
+ });
+ }
+
+ static Task pressKey(int keyCode) {
+ return AbstractTask.create(task -> {
+ task.press(keyCode);
+ task.runLater(() -> task.future.set(null));
+ });
+ }
+
+ static Task pressKey(char character, int keyCode) {
+ return AbstractTask.create(task -> {
+ task.press(character, keyCode);
+ task.runLater(() -> task.future.set(null));
+ });
+ }
+}
diff --git a/src/integration-test/java/com/replaymod/core/Utils.java b/src/integration-test/java/com/replaymod/core/Utils.java
new file mode 100644
index 00000000..61d41497
--- /dev/null
+++ b/src/integration-test/java/com/replaymod/core/Utils.java
@@ -0,0 +1,9 @@
+package com.replaymod.core;
+
+public class Utils {
+ public static void times(int x, Runnable runnable) {
+ for (int i = 0; i < x; i++) {
+ runnable.run();
+ }
+ }
+}
diff --git a/src/integration-test/java/com/replaymod/core/Wait.java b/src/integration-test/java/com/replaymod/core/Wait.java
new file mode 100644
index 00000000..b528889d
--- /dev/null
+++ b/src/integration-test/java/com/replaymod/core/Wait.java
@@ -0,0 +1,21 @@
+package com.replaymod.core;
+
+public class Wait extends AbstractTask {
+ private final int duration;
+
+ public Wait(int duration) {
+ this.duration = duration;
+ }
+
+ @Override
+ protected void init() {
+ new Thread(() -> {
+ try {
+ Thread.sleep(duration);
+ runLater(() -> future.set(null));
+ } catch (InterruptedException e) {
+ runLater(() -> future.setException(e));
+ }
+ }).start();
+ }
+}
diff --git a/src/integration-test/java/com/replaymod/core/regression/RegressionTest60.java b/src/integration-test/java/com/replaymod/core/regression/RegressionTest60.java
new file mode 100644
index 00000000..72603462
--- /dev/null
+++ b/src/integration-test/java/com/replaymod/core/regression/RegressionTest60.java
@@ -0,0 +1,59 @@
+package com.replaymod.core.regression;
+
+import com.replaymod.core.AbstractTask;
+import com.replaymod.core.CompositeTask;
+import com.replaymod.core.Task;
+import com.replaymod.replay.SpectatePlayer;
+import com.replaymod.replay.overlay.OverlayGui;
+import com.replaymod.simplepathing.GuiPathingTasks;
+import com.replaymod.simplepathing.gui.GuiEditKeyframe;
+import org.lwjgl.input.Keyboard;
+
+import static com.replaymod.core.AbstractTask.mc;
+import static com.replaymod.core.Utils.times;
+
+/**
+ * Regression test: #60 Crash in path preview when two spectator keyframes are closer than 50ms
+ */
+public class RegressionTest60 extends CompositeTask {
+ public RegressionTest60() {
+ super(new Task[]{
+ new SpectatePlayer(),
+ OverlayGui.whileOpened(
+ // Place first spectator keyframe
+ new GuiPathingTasks.ClickPositionKeyframeButton(),
+ // Place second spectator keyframe
+ Task.click(130, 50),
+ new GuiPathingTasks.ClickPositionKeyframeButton(),
+ // Move second keyframe to 20ms on the keyframe timeline
+ AbstractTask.create(task -> {
+ // Double click keyframe
+ task.click(130, 50);
+ task.click(130, 50);
+ task.expectGui(GuiEditKeyframe.Spectator.class, gui -> {
+ // Set ms field to 20
+ task.click(mc.currentScreen.width / 2 + 80, mc.currentScreen.height / 2);
+ times(4, () -> task.press(Keyboard.KEY_BACK));
+ task.type("20");
+ // Set other fields to 0
+ times(2, () -> {
+ task.press(Keyboard.KEY_TAB);
+ times(4, () -> task.press(Keyboard.KEY_BACK));
+ task.press(Keyboard.KEY_0);
+ });
+ task.click(gui.saveButton);
+ task.expectPopupClosed(() -> task.future.set(null));
+ });
+ }),
+ // Place second spectator keyframe
+ new GuiPathingTasks.ClickPositionKeyframeButton()
+ ),
+ // Stop spectating player
+ new SpectatePlayer.End(),
+ // Enable path preview
+ Task.pressKey(Keyboard.KEY_H),
+ new GuiPathingTasks.ClearKeyframeTimeline(),
+ Task.pressKey(Keyboard.KEY_H),
+ });
+ }
+}
diff --git a/src/integration-test/java/com/replaymod/core/regression/RegressionTest61.java b/src/integration-test/java/com/replaymod/core/regression/RegressionTest61.java
new file mode 100644
index 00000000..5b5b4951
--- /dev/null
+++ b/src/integration-test/java/com/replaymod/core/regression/RegressionTest61.java
@@ -0,0 +1,35 @@
+package com.replaymod.core.regression;
+
+import com.replaymod.core.AbstractTask;
+import com.replaymod.core.CompositeTask;
+import com.replaymod.core.Task;
+import com.replaymod.replay.overlay.OverlayGui;
+import com.replaymod.simplepathing.GuiPathingTasks;
+import com.replaymod.simplepathing.gui.GuiEditKeyframe;
+
+/**
+ * Regression test: #61 NPE when saving in edit keyframe gui of last keyframe
+ */
+public class RegressionTest61 extends CompositeTask {
+ public RegressionTest61() {
+ super(new Task[]{
+ OverlayGui.whileOpened(
+ // Place keyframe
+ Task.click(130, 50),
+ new GuiPathingTasks.ClickPositionKeyframeButton(),
+ AbstractTask.create(task -> {
+ // Double click keyframe
+ task.click(130, 50);
+ task.click(130, 50);
+ task.expectGui(GuiEditKeyframe.Position.class, gui -> {
+ task.click(gui.saveButton);
+ task.expectPopupClosed(() -> task.future.set(null));
+ });
+ }),
+ // Place second spectator keyframe
+ new GuiPathingTasks.ClickPositionKeyframeButton()
+ ),
+ new GuiPathingTasks.ClearKeyframeTimeline(),
+ });
+ }
+}
diff --git a/src/integration-test/java/com/replaymod/core/regression/RegressionTest62.java b/src/integration-test/java/com/replaymod/core/regression/RegressionTest62.java
new file mode 100644
index 00000000..ebfa44a3
--- /dev/null
+++ b/src/integration-test/java/com/replaymod/core/regression/RegressionTest62.java
@@ -0,0 +1,28 @@
+package com.replaymod.core.regression;
+
+import com.replaymod.core.CompositeTask;
+import com.replaymod.core.Task;
+import com.replaymod.replay.overlay.OverlayGui;
+import com.replaymod.simplepathing.GuiPathingTasks;
+
+/**
+ * Regression test: #62 Swapping the only two existing keyframes causes NPE
+ */
+public class RegressionTest62 extends CompositeTask {
+ public RegressionTest62() {
+ super(new Task[]{
+ OverlayGui.whileOpened(
+ // Place first keyframe
+ Task.click(130, 50),
+ new GuiPathingTasks.ClickPositionKeyframeButton(),
+ // Place second keyframe
+ Task.click(150, 50),
+ new GuiPathingTasks.ClickPositionKeyframeButton(),
+ // Move first keyframe past second keyframe
+ Task.click(130,50),
+ Task.drag(170,50)
+ ),
+ new GuiPathingTasks.ClearKeyframeTimeline()
+ });
+ }
+}
diff --git a/src/integration-test/java/com/replaymod/extra/DownloadOpenEye.java b/src/integration-test/java/com/replaymod/extra/DownloadOpenEye.java
new file mode 100644
index 00000000..753090c0
--- /dev/null
+++ b/src/integration-test/java/com/replaymod/extra/DownloadOpenEye.java
@@ -0,0 +1,25 @@
+package com.replaymod.extra;
+
+import com.replaymod.core.AbstractTask;
+import com.replaymod.extras.OpenEyeExtra;
+import net.minecraftforge.fml.common.Loader;
+
+import java.io.File;
+import java.nio.file.NoSuchFileException;
+
+public class DownloadOpenEye extends AbstractTask {
+ @Override
+ protected void init() {
+ expectGui(OpenEyeExtra.OfferGui.class, offerGui -> {
+ click(offerGui.yesButton);
+ expectGuiClosed(20 * 1000, () -> {
+ File targetFile = new File(mc.mcDataDir, "mods/" + Loader.MC_VERSION + "/OpenEye.jar");
+ if (!targetFile.exists()) {
+ future.setException(new NoSuchFileException(targetFile.getAbsolutePath()));
+ } else {
+ future.set(null);
+ }
+ });
+ });
+ }
+}
diff --git a/src/integration-test/java/com/replaymod/online/SkipLogin.java b/src/integration-test/java/com/replaymod/online/SkipLogin.java
new file mode 100644
index 00000000..733b6543
--- /dev/null
+++ b/src/integration-test/java/com/replaymod/online/SkipLogin.java
@@ -0,0 +1,14 @@
+package com.replaymod.online;
+
+import com.replaymod.core.AbstractTask;
+import com.replaymod.online.gui.GuiLoginPrompt;
+
+public class SkipLogin extends AbstractTask {
+ @Override
+ protected void init() {
+ expectGui(GuiLoginPrompt.class, gui -> {
+ click(gui.cancelButton);
+ expectGuiClosed(() -> future.set(null));
+ });
+ }
+}
diff --git a/src/integration-test/java/com/replaymod/recording/CreateSPWorld.java b/src/integration-test/java/com/replaymod/recording/CreateSPWorld.java
new file mode 100644
index 00000000..a6a4e684
--- /dev/null
+++ b/src/integration-test/java/com/replaymod/recording/CreateSPWorld.java
@@ -0,0 +1,32 @@
+package com.replaymod.recording;
+
+import com.replaymod.core.AbstractTask;
+import net.minecraft.client.gui.GuiCreateWorld;
+import net.minecraft.client.gui.GuiMainMenu;
+import net.minecraft.client.gui.GuiWorldSelection;
+import net.minecraftforge.client.event.RenderGameOverlayEvent;
+import net.minecraftforge.common.MinecraftForge;
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
+
+public class CreateSPWorld extends AbstractTask {
+ @Override
+ protected void init() {
+ expectGui(GuiMainMenu.class, mainMenu -> {
+ click("Singleplayer");
+ expectGui(GuiWorldSelection.class, selectWorld -> {
+ click("Create New World");
+ expectGui(GuiCreateWorld.class, createWorld -> {
+ click("Create New World");
+ class EventHandler {
+ @SubscribeEvent
+ public void onRenderIngame(RenderGameOverlayEvent.Pre event) {
+ MinecraftForge.EVENT_BUS.unregister(this);
+ runLater(() -> future.set(null));
+ }
+ }
+ MinecraftForge.EVENT_BUS.register(new EventHandler());
+ });
+ });
+ });
+ }
+}
diff --git a/src/integration-test/java/com/replaymod/recording/ExitSPWorld.java b/src/integration-test/java/com/replaymod/recording/ExitSPWorld.java
new file mode 100644
index 00000000..b531a275
--- /dev/null
+++ b/src/integration-test/java/com/replaymod/recording/ExitSPWorld.java
@@ -0,0 +1,36 @@
+package com.replaymod.recording;
+
+import com.replaymod.core.AbstractTask;
+import net.minecraft.client.gui.GuiIngameMenu;
+import net.minecraft.client.gui.GuiMainMenu;
+import org.apache.commons.io.filefilter.DirectoryFileFilter;
+
+import java.io.IOException;
+
+public class ExitSPWorld extends AbstractTask {
+ @Override
+ protected void init() {
+ mc.displayInGameMenu();
+ expectGui(GuiIngameMenu.class, ingameMenu -> {
+ click("Save and Quit to Title");
+ expectGui(GuiMainMenu.class, mainMenu -> new Thread(() -> {
+ try {
+ while (true) {
+ String[] dirs = core.getReplayFolder().list(DirectoryFileFilter.DIRECTORY);
+ if (dirs == null) {
+ future.setException(new NullPointerException("dirs is null"));
+ return;
+ }
+ if (dirs.length == 0) {
+ runLater(() -> future.set(null));
+ return;
+ }
+ Thread.sleep(10);
+ }
+ } catch (IOException | InterruptedException e) {
+ future.setException(e);
+ }
+ }).start());
+ });
+ }
+}
diff --git a/src/integration-test/java/com/replaymod/replay/ExitReplay.java b/src/integration-test/java/com/replaymod/replay/ExitReplay.java
new file mode 100644
index 00000000..5989e0ef
--- /dev/null
+++ b/src/integration-test/java/com/replaymod/replay/ExitReplay.java
@@ -0,0 +1,16 @@
+package com.replaymod.replay;
+
+import com.replaymod.core.AbstractTask;
+import net.minecraft.client.gui.GuiIngameMenu;
+import net.minecraft.client.gui.GuiMainMenu;
+
+public class ExitReplay extends AbstractTask {
+ @Override
+ protected void init() {
+ mc.displayInGameMenu();
+ expectGui(GuiIngameMenu.class, ingameMenu -> {
+ click("Exit Replay");
+ expectGui(GuiMainMenu.class, mainMenu -> future.set(null));
+ });
+ }
+}
diff --git a/src/integration-test/java/com/replaymod/replay/LoadReplay.java b/src/integration-test/java/com/replaymod/replay/LoadReplay.java
new file mode 100644
index 00000000..2db6383b
--- /dev/null
+++ b/src/integration-test/java/com/replaymod/replay/LoadReplay.java
@@ -0,0 +1,32 @@
+package com.replaymod.replay;
+
+import com.replaymod.core.AbstractTask;
+import com.replaymod.replay.gui.screen.GuiReplayViewer;
+import de.johni0702.minecraft.gui.function.Clickable;
+import net.minecraftforge.client.event.RenderGameOverlayEvent;
+import net.minecraftforge.common.MinecraftForge;
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
+import org.lwjgl.util.Point;
+import org.lwjgl.util.ReadableDimension;
+
+public class LoadReplay extends AbstractTask {
+ @Override
+ protected void init() {
+ expectGui(GuiReplayViewer.class, replayViewer -> runLater(() -> {
+ ReadableDimension size = replayViewer.getMaxSize();
+ // Select first entry
+ replayViewer.forEach(Clickable.class).mouseClick(new Point(size.getWidth() / 2, 40), 0);
+ // Load first replay
+ click(replayViewer.loadButton);
+
+ class EventHandler {
+ @SubscribeEvent
+ public void onRenderIngame(RenderGameOverlayEvent.Pre event) {
+ MinecraftForge.EVENT_BUS.unregister(this);
+ runLater(() -> future.set(null));
+ }
+ }
+ MinecraftForge.EVENT_BUS.register(new EventHandler());
+ }));
+ }
+}
diff --git a/src/integration-test/java/com/replaymod/replay/OpenReplayViewer.java b/src/integration-test/java/com/replaymod/replay/OpenReplayViewer.java
new file mode 100644
index 00000000..fb0e5b50
--- /dev/null
+++ b/src/integration-test/java/com/replaymod/replay/OpenReplayViewer.java
@@ -0,0 +1,15 @@
+package com.replaymod.replay;
+
+import com.replaymod.core.AbstractTask;
+import com.replaymod.replay.gui.screen.GuiReplayViewer;
+import net.minecraft.client.gui.GuiMainMenu;
+
+public class OpenReplayViewer extends AbstractTask {
+ @Override
+ protected void init() {
+ expectGui(GuiMainMenu.class, mainMenu -> {
+ click("Replay Viewer");
+ expectGui(GuiReplayViewer.class, replayViewer -> future.set(null));
+ });
+ }
+}
diff --git a/src/integration-test/java/com/replaymod/replay/SpectatePlayer.java b/src/integration-test/java/com/replaymod/replay/SpectatePlayer.java
new file mode 100644
index 00000000..c248333b
--- /dev/null
+++ b/src/integration-test/java/com/replaymod/replay/SpectatePlayer.java
@@ -0,0 +1,42 @@
+package com.replaymod.replay;
+
+import com.replaymod.core.AbstractTask;
+import com.replaymod.extras.playeroverview.PlayerOverviewGui;
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
+import net.minecraftforge.fml.common.gameevent.TickEvent;
+import org.lwjgl.input.Keyboard;
+
+import java.util.concurrent.TimeoutException;
+
+public class SpectatePlayer extends AbstractTask {
+ @Override
+ protected void init() {
+ press(Keyboard.KEY_B);
+ expectGui(PlayerOverviewGui.class, overview -> {
+ click(overview.getMaxSize().getWidth() / 2 - 60, 60);
+ press(Keyboard.KEY_ESCAPE);
+ expectGuiClosed(() -> future.set(null));
+ });
+ }
+
+ public static class End extends AbstractTask {
+ private int timeout;
+
+ @Override
+ protected void init() {
+ runLater(() -> press(Keyboard.KEY_LSHIFT));
+ }
+
+ @SubscribeEvent
+ public void onTick(TickEvent.RenderTickEvent event) {
+ if (event.phase != TickEvent.Phase.START) return;
+ if (timeout++ > 20) {
+ future.setException(new TimeoutException("Camera hasn't stopped spectating."));
+ return;
+ }
+ if (mc.getRenderViewEntity() == mc.player) {
+ future.set(null);
+ }
+ }
+ }
+}
diff --git a/src/integration-test/java/com/replaymod/replay/overlay/OverlayGui.java b/src/integration-test/java/com/replaymod/replay/overlay/OverlayGui.java
new file mode 100644
index 00000000..9e261082
--- /dev/null
+++ b/src/integration-test/java/com/replaymod/replay/overlay/OverlayGui.java
@@ -0,0 +1,33 @@
+package com.replaymod.replay.overlay;
+
+import com.replaymod.core.AbstractTask;
+import com.replaymod.core.CompositeTask;
+import com.replaymod.core.Task;
+import com.replaymod.replay.gui.overlay.GuiReplayOverlay;
+import org.lwjgl.input.Keyboard;
+
+public class OverlayGui {
+ public static Task whileOpened(Task...tasks) {
+ Task[] nTasks = new Task[tasks.length + 2];
+ System.arraycopy(tasks, 0, nTasks, 1, tasks.length);
+ nTasks[0] = new Open();
+ nTasks[tasks.length + 1] = new Close();
+ return new CompositeTask(nTasks);
+ }
+
+ public static class Open extends AbstractTask {
+ @Override
+ protected void init() {
+ press(Keyboard.KEY_T);
+ expectGui(GuiReplayOverlay.class, done -> future.set(null));
+ }
+ }
+
+ public static class Close extends AbstractTask {
+ @Override
+ protected void init() {
+ press(Keyboard.KEY_ESCAPE);
+ expectGuiClosed(() -> future.set(null));
+ }
+ }
+}
diff --git a/src/integration-test/java/com/replaymod/simplepathing/GuiPathingTasks.java b/src/integration-test/java/com/replaymod/simplepathing/GuiPathingTasks.java
new file mode 100644
index 00000000..2e88ac6b
--- /dev/null
+++ b/src/integration-test/java/com/replaymod/simplepathing/GuiPathingTasks.java
@@ -0,0 +1,35 @@
+package com.replaymod.simplepathing;
+
+import com.replaymod.core.AbstractTask;
+import com.replaymod.replay.gui.overlay.GuiReplayOverlay;
+import com.replaymod.simplepathing.gui.GuiPathing;
+import de.johni0702.minecraft.gui.popup.GuiYesNoPopup;
+import org.lwjgl.input.Keyboard;
+
+public abstract class GuiPathingTasks extends AbstractTask {
+ @Override
+ protected void init() {
+ expectGui(GuiReplayOverlay.class, ign -> init0(ReplayModSimplePathing.instance.getGuiPathing()));
+ }
+
+ protected abstract void init0(GuiPathing guiPathing);
+
+ public static class ClickPositionKeyframeButton extends GuiPathingTasks {
+ @Override
+ protected void init0(GuiPathing guiPathing) {
+ click(guiPathing.positionKeyframeButton);
+ runLater(() -> future.set(null));
+ }
+ }
+
+ public static class ClearKeyframeTimeline extends AbstractTask {
+ @Override
+ protected void init() {
+ press(Keyboard.KEY_C);
+ expectGui(GuiYesNoPopup.class, popup -> {
+ click(popup.getYesButton());
+ expectGuiClosed(() -> future.set(null));
+ });
+ }
+ }
+}
diff --git a/src/main/java/com/replaymod/core/utils/Utils.java b/src/main/java/com/replaymod/core/utils/Utils.java
index 84f6f69b..bbc24f35 100644
--- a/src/main/java/com/replaymod/core/utils/Utils.java
+++ b/src/main/java/com/replaymod/core/utils/Utils.java
@@ -2,6 +2,7 @@ package com.replaymod.core.utils;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
import de.johni0702.minecraft.gui.GuiRenderer;
import de.johni0702.minecraft.gui.RenderInfo;
import de.johni0702.minecraft.gui.container.AbstractGuiScrollable;
@@ -25,6 +26,7 @@ import org.lwjgl.input.Keyboard;
import org.lwjgl.util.Dimension;
import org.lwjgl.util.ReadableDimension;
+import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.imageio.ImageIO;
import javax.net.ssl.SSLContext;
@@ -45,6 +47,7 @@ import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.UUID;
+import java.util.function.Consumer;
import static net.minecraft.client.Minecraft.getMinecraft;
@@ -149,6 +152,20 @@ public class Utils {
return Keyboard.isKeyDown(Keyboard.KEY_LCONTROL) || Keyboard.isKeyDown(Keyboard.KEY_RCONTROL);
}
+ public static void addCallback(ListenableFuture future, Consumer onSuccess, Consumer onFailure) {
+ Futures.addCallback(future, new FutureCallback() {
+ @Override
+ public void onSuccess(@Nullable T result) {
+ onSuccess.accept(result);
+ }
+
+ @Override
+ public void onFailure(@Nonnull Throwable t) {
+ onFailure.accept(t);
+ }
+ });
+ }
+
public static GuiInfoPopup error(Logger logger, GuiContainer container, CrashReport crashReport, Runnable onClose) {
// Convert crash report to string
String crashReportStr = crashReport.getCompleteReport();
diff --git a/src/main/java/com/replaymod/editor/gui/GuiReplayEditor.java b/src/main/java/com/replaymod/editor/gui/GuiReplayEditor.java
index 62b027ab..65ea1ed0 100644
--- a/src/main/java/com/replaymod/editor/gui/GuiReplayEditor.java
+++ b/src/main/java/com/replaymod/editor/gui/GuiReplayEditor.java
@@ -59,6 +59,9 @@ public class GuiReplayEditor extends GuiScreen {
public GuiButton currentTabButton;
public GuiPanel currentTabPanel;
+ public final GuiLabel warningLabel = new GuiLabel(this).setColor(Colors.RED)
+ .setI18nText("replaymod.gui.editor.disclaimer");
+
public final GuiPanel tabButtons = new GuiPanel(this).setLayout(new GridLayout().setSpacingX(5));
public final List tabPanels = new ArrayList<>();
@@ -84,15 +87,18 @@ public class GuiReplayEditor extends GuiScreen {
// Move all inactive panels aside
tabPanels.forEach(e -> pos(e, Integer.MIN_VALUE, Integer.MIN_VALUE));
+ pos(warningLabel, 10, 22);
+ size(warningLabel, width - 20, 10);
+
+ pos(tabButtons, 10, y(warningLabel) + height(warningLabel) + 2);
+ size(tabButtons, width - 20, 20);
+
pos(buttonPanel, width - 10 - width(buttonPanel), height - 10 - height(buttonPanel));
if (currentTabPanel != null) {
- pos(currentTabPanel, 10, 50);
+ pos(currentTabPanel, 10, y(tabButtons) + height(tabButtons) + 10);
size(currentTabPanel, width - 20, y(buttonPanel) - 10 - y(currentTabPanel));
}
-
- pos(tabButtons, 10, 20);
- size(tabButtons, width - 20, 20);
}
});
}
diff --git a/src/main/java/com/replaymod/extras/LocalizationExtra.java b/src/main/java/com/replaymod/extras/LocalizationExtra.java
index 56eef1a4..27f0eb19 100644
--- a/src/main/java/com/replaymod/extras/LocalizationExtra.java
+++ b/src/main/java/com/replaymod/extras/LocalizationExtra.java
@@ -1,102 +1,106 @@
package com.replaymod.extras;
-import com.google.common.base.Charsets;
import com.google.common.collect.ImmutableSet;
import com.replaymod.core.ReplayMod;
-import com.replaymod.online.ReplayModOnline;
-import com.replaymod.online.api.ApiClient;
-import com.replaymod.online.api.ApiException;
-import lombok.RequiredArgsConstructor;
import net.minecraft.client.Minecraft;
import net.minecraft.client.resources.IResourcePack;
import net.minecraft.client.resources.data.IMetadataSection;
import net.minecraft.client.resources.data.MetadataSerializer;
import net.minecraft.util.ResourceLocation;
-import org.apache.commons.lang3.StringEscapeUtils;
+import org.apache.commons.io.IOUtils;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
-import java.net.ConnectException;
+import java.net.URL;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+import static com.replaymod.extras.ReplayModExtras.LOGGER;
public class LocalizationExtra implements Extra {
- private ReplayModOnline module;
+ private static final String ZIP_FILE_URL = "https://github.com/ReplayMod/Translations/archive/master.zip";
+ private static final String LANG_PREFIX = "Translations-master/";
@Override
public void register(ReplayMod mod) throws Exception {
- this.module = ReplayModOnline.instance;
-
final Minecraft mc = mod.getMinecraft();
- Thread localizedResourcePackLoader = new Thread(new Runnable() {
- @Override
- public void run() {
- try {
+ if (Boolean.parseBoolean(System.getProperty("replaymod.offline", "false"))) {
+ return;
+ }
+ Thread localizedResourcePackLoader = new Thread(() -> {
+ try {
+ // Download zip of lang files
+ LOGGER.debug("Downloading languages from {}", ZIP_FILE_URL);
+ Map languages = new HashMap<>();
+ try (InputStream urlIn = new URL(ZIP_FILE_URL).openStream();
+ ZipInputStream in = new ZipInputStream(urlIn)) {
+ ZipEntry entry;
+ while ((entry = in.getNextEntry()) != null) {
+ String name = entry.getName();
+ if (!name.startsWith(LANG_PREFIX) || !name.endsWith(".lang")) {
+ continue;
+ }
+ name = name.substring(LANG_PREFIX.length());
+ languages.put(name, IOUtils.toByteArray(in));
+ LOGGER.debug("Added language file {}", name);
+ }
+ }
+ LOGGER.debug("Downloaded {} languages", languages.size());
+
+ // Add lang files as resource pack
+ mc.addScheduledTask(() -> {
@SuppressWarnings("unchecked")
List defaultResourcePacks = mc.defaultResourcePacks;
- defaultResourcePacks.add(new LocalizedResourcePack(module.getApiClient()));
- mc.addScheduledTask(new Runnable() {
- @Override
- public void run() {
- mc.refreshResources();
- }
- });
- } catch(Exception e) {
- e.printStackTrace();
- }
+ defaultResourcePacks.add(new LocalizedResourcePack(languages));
+ mc.getLanguageManager().onResourceManagerReload(mc.getResourceManager());
+ LOGGER.debug("Added language files to resource packs and reloaded LanguageManager");
+ });
+ } catch (Throwable t) {
+ LOGGER.error("Loading localized resource pack:", t);
}
}, "localizedResourcePackLoader");
+ localizedResourcePackLoader.setDaemon(true);
localizedResourcePackLoader.start();
}
- @RequiredArgsConstructor
public static class LocalizedResourcePack implements IResourcePack {
- private final ApiClient apiClient;
- private Map availableLanguages = new HashMap<>();
- private boolean websiteAvailable = true;
+ private final Pattern LANG_PATTERN = Pattern.compile("^lang/([.+].lang)$");
+ private final Map languages;
+
+ public LocalizedResourcePack(Map languages) {
+ this.languages = languages;
+ }
@Override
public InputStream getInputStream(ResourceLocation loc) {
- if(!loc.getResourcePath().endsWith(".lang")) return null;
- String langcode = loc.getResourcePath().split("/")[1].split("\\.")[0];
- if(availableLanguages.containsKey(langcode)) return new ByteArrayInputStream(availableLanguages.get(langcode).getBytes(Charsets.UTF_8));
+ if (!"replaymod".equals(loc.getResourceDomain())) return null;
+ Matcher matcher = LANG_PATTERN.matcher(loc.getResourcePath());
+ if (matcher.matches()) {
+ byte[] bytes = languages.get(matcher.group());
+ if (bytes != null) {
+ return new ByteArrayInputStream(bytes);
+ }
+ }
return null;
}
@Override
public boolean resourceExists(ResourceLocation loc) {
- if(!(loc.getResourcePath().endsWith(".lang"))) return false;
- String langcode = loc.getResourcePath().split("/")[1].split("\\.")[0];
- if(availableLanguages.containsKey(langcode)) return true;
- if(!websiteAvailable) return false;
- try {
- if (Boolean.parseBoolean(System.getProperty("replaymod.offline", "false"))) {
- return false;
- }
- String lang = apiClient.getTranslation(langcode);
- String prop = StringEscapeUtils.unescapeHtml4(lang);
- availableLanguages.put(langcode, prop);
- return true;
- } catch (ApiException e) {
- if (e.getError() == null || e.getError().getId() != 16) { // This language has not been translated
- e.printStackTrace();
- }
- } catch(ConnectException ce) {
- websiteAvailable = false;
- ce.printStackTrace();
- } catch(Exception e) {
- e.printStackTrace();
- }
- return false;
+ // Assumes that getInputStream returns a ByteArrayInputStream that doesn't need to be closed
+ return getInputStream(loc) != null;
}
@Override
public Set getResourceDomains() {
- return ImmutableSet.of("minecraft", "replaymod");
+ return ImmutableSet.of("replaymod");
}
@Override
diff --git a/src/main/java/com/replaymod/extras/OpenEyeExtra.java b/src/main/java/com/replaymod/extras/OpenEyeExtra.java
index ad5c1004..ba0575fb 100644
--- a/src/main/java/com/replaymod/extras/OpenEyeExtra.java
+++ b/src/main/java/com/replaymod/extras/OpenEyeExtra.java
@@ -43,7 +43,7 @@ public class OpenEyeExtra implements Extra {
}
}
- private class OfferGui extends AbstractGuiScreen {
+ public class OfferGui extends AbstractGuiScreen {
public final GuiScreen parent;
public final GuiPanel textPanel = new GuiPanel().setLayout(new VerticalLayout().setSpacing(3))
.addElements(new VerticalLayout.Data(0.5),
@@ -69,7 +69,7 @@ public class OpenEyeExtra implements Extra {
GuiPopup popup = new GuiPopup(OfferGui.this);
new Thread(() -> {
try {
- File targetFile = new File("mods/" + Loader.MC_VERSION, "OpenEye.jar");
+ File targetFile = new File(mod.getMinecraft().mcDataDir, "mods/" + Loader.MC_VERSION + "/OpenEye.jar");
FileUtils.forceMkdir(targetFile.getParentFile());
HttpsURLConnection connection = (HttpsURLConnection) new URL(DOWNLOAD_URL).openConnection();
@@ -107,7 +107,7 @@ public class OpenEyeExtra implements Extra {
}
}
- private static final class GuiPopup extends AbstractGuiPopup {
+ public static final class GuiPopup extends AbstractGuiPopup {
GuiPopup(GuiContainer container) {
super(container);
popup.addElements(null, new GuiIndicator().setColor(Colors.BLACK));
diff --git a/src/main/java/com/replaymod/extras/ReplayModExtras.java b/src/main/java/com/replaymod/extras/ReplayModExtras.java
index e82eff09..3ec83875 100644
--- a/src/main/java/com/replaymod/extras/ReplayModExtras.java
+++ b/src/main/java/com/replaymod/extras/ReplayModExtras.java
@@ -32,11 +32,11 @@ public class ReplayModExtras {
OpenEyeExtra.class
);
- private Logger logger;
+ public static Logger LOGGER;
@Mod.EventHandler
public void preInit(FMLPreInitializationEvent event) {
- logger = event.getModLog();
+ LOGGER = event.getModLog();
}
@Mod.EventHandler
@@ -46,7 +46,7 @@ public class ReplayModExtras {
Extra extra = cls.newInstance();
extra.register(ReplayMod.instance);
} catch (Throwable t) {
- logger.warn("Failed to load extra " + cls.getName() + ": ", t);
+ LOGGER.warn("Failed to load extra " + cls.getName() + ": ", t);
}
}
}
diff --git a/src/main/java/com/replaymod/extras/playeroverview/PlayerOverviewGui.java b/src/main/java/com/replaymod/extras/playeroverview/PlayerOverviewGui.java
index c99bfc6f..7e0ce988 100644
--- a/src/main/java/com/replaymod/extras/playeroverview/PlayerOverviewGui.java
+++ b/src/main/java/com/replaymod/extras/playeroverview/PlayerOverviewGui.java
@@ -4,10 +4,15 @@ import com.replaymod.core.utils.Utils;
import com.replaymod.replay.ReplayModReplay;
import de.johni0702.minecraft.gui.GuiRenderer;
import de.johni0702.minecraft.gui.RenderInfo;
-import de.johni0702.minecraft.gui.container.*;
+import de.johni0702.minecraft.gui.container.GuiClickable;
+import de.johni0702.minecraft.gui.container.GuiContainer;
+import de.johni0702.minecraft.gui.container.GuiPanel;
+import de.johni0702.minecraft.gui.container.GuiScreen;
+import de.johni0702.minecraft.gui.container.GuiVerticalList;
import de.johni0702.minecraft.gui.element.GuiCheckbox;
import de.johni0702.minecraft.gui.element.GuiImage;
import de.johni0702.minecraft.gui.element.GuiLabel;
+import de.johni0702.minecraft.gui.element.GuiTooltip;
import de.johni0702.minecraft.gui.element.IGuiCheckbox;
import de.johni0702.minecraft.gui.function.Closeable;
import de.johni0702.minecraft.gui.layout.CustomLayout;
@@ -37,6 +42,7 @@ public class PlayerOverviewGui extends GuiScreen implements Closeable {
public final GuiVerticalList playersScrollable = new GuiVerticalList(contentPanel)
.setDrawSlider(true).setDrawShadow(true);
public final GuiCheckbox saveCheckbox = new GuiCheckbox(contentPanel)
+ .setTooltip(new GuiTooltip().setI18nText("replaymod.gui.playeroverview.remembersettings.description"))
.setI18nLabel("replaymod.gui.playeroverview.remembersettings");
public final GuiCheckbox checkAll = new GuiCheckbox(contentPanel){
@Override
@@ -44,14 +50,14 @@ public class PlayerOverviewGui extends GuiScreen implements Closeable {
getMinecraft().getSoundHandler().playSound(PositionedSoundRecord.getMasterRecord(SoundEvents.UI_BUTTON_CLICK, 1.0F));
playersScrollable.forEach(IGuiCheckbox.class).setChecked(true);
}
- }.setLabel("").setChecked(true);
+ }.setLabel("").setChecked(true).setTooltip(new GuiTooltip().setI18nText("replaymod.gui.playeroverview.showall"));
public final GuiCheckbox uncheckAll = new GuiCheckbox(contentPanel){
@Override
public void onClick() {
getMinecraft().getSoundHandler().playSound(PositionedSoundRecord.getMasterRecord(SoundEvents.UI_BUTTON_CLICK, 1.0F));
playersScrollable.forEach(IGuiCheckbox.class).setChecked(false);
}
- }.setLabel("").setChecked(false);
+ }.setLabel("").setChecked(false).setTooltip(new GuiTooltip().setI18nText("replaymod.gui.playeroverview.hideall"));
{
setBackground(Background.NONE);
diff --git a/src/main/java/com/replaymod/online/ReplayModOnline.java b/src/main/java/com/replaymod/online/ReplayModOnline.java
index 6e915b9c..afafa4d7 100644
--- a/src/main/java/com/replaymod/online/ReplayModOnline.java
+++ b/src/main/java/com/replaymod/online/ReplayModOnline.java
@@ -82,7 +82,7 @@ public class ReplayModOnline {
// Initial login prompt
if (!core.getSettingsRegistry().get(Setting.SKIP_LOGIN_PROMPT)) {
if (!isLoggedIn()) {
- new GuiLoginPrompt(apiClient, null, null, false).display();
+ core.runLater(() -> new GuiLoginPrompt(apiClient, GuiScreen.wrap(getMinecraft().currentScreen), null, false).display());
}
}
}
diff --git a/src/main/java/com/replaymod/online/gui/GuiLoginPrompt.java b/src/main/java/com/replaymod/online/gui/GuiLoginPrompt.java
index aeccce8f..f85080d1 100755
--- a/src/main/java/com/replaymod/online/gui/GuiLoginPrompt.java
+++ b/src/main/java/com/replaymod/online/gui/GuiLoginPrompt.java
@@ -21,7 +21,7 @@ public class GuiLoginPrompt extends AbstractGuiScreen {
private GuiLabel noAccountLabel = new GuiLabel(this).setI18nText("replaymod.gui.login.noacc");
private GuiLabel statusLabel = new GuiLabel(this);
private GuiButton loginButton = new GuiButton(this).setI18nLabel("replaymod.gui.login").setSize(150, 20).setEnabled(false);
- private GuiButton cancelButton = new GuiButton(this).setI18nLabel("replaymod.gui.cancel").setSize(150, 20);
+ public GuiButton cancelButton = new GuiButton(this).setI18nLabel("replaymod.gui.cancel").setSize(150, 20);
private GuiButton registerButton = new GuiButton(this).setI18nLabel("replaymod.gui.register").setSize(150, 20);
private GuiTextField username = new GuiTextField(this).setSize(145, 20).setMaxLength(16).setFocused(true);
private GuiPasswordField password = new GuiPasswordField(this).setSize(145, 20)
diff --git a/src/main/java/com/replaymod/online/gui/GuiReplayCenter.java b/src/main/java/com/replaymod/online/gui/GuiReplayCenter.java
index c7e375fe..2470bd2d 100644
--- a/src/main/java/com/replaymod/online/gui/GuiReplayCenter.java
+++ b/src/main/java/com/replaymod/online/gui/GuiReplayCenter.java
@@ -28,6 +28,7 @@ import de.johni0702.minecraft.gui.container.GuiScreen;
import de.johni0702.minecraft.gui.element.GuiButton;
import de.johni0702.minecraft.gui.element.GuiImage;
import de.johni0702.minecraft.gui.element.GuiLabel;
+import de.johni0702.minecraft.gui.element.GuiTooltip;
import de.johni0702.minecraft.gui.element.IGuiButton;
import de.johni0702.minecraft.gui.element.advanced.GuiResourceLoadingList;
import de.johni0702.minecraft.gui.layout.CustomLayout;
@@ -109,6 +110,7 @@ public class GuiReplayCenter extends GuiScreen {
public void run() {
GuiReplayEntry selected = list.getSelected();
replayButtonPanel.forEach(IGuiButton.class).setEnabled(selected != null);
+ replayButtonPanel.forEach(IGuiButton.class).setTooltip(null);
if (selected != null) {
int replayId = selected.fileInfo.getId();
boolean favorited = favoritedReplays.contains(replayId);
@@ -123,10 +125,22 @@ public class GuiReplayCenter extends GuiScreen {
favoriteButton.setI18nLabel("replaymod.gui.center." + (favorited ? "unfavorite" : "favorite"));
// Only allow button usage for either unfavorite or favorite after they've actually downloaded it
favoriteButton.setEnabled(favorited || selected.downloaded);
+ if (favoriteButton.isEnabled()) {
+ favoriteButton.setTooltip(null);
+ } else {
+ favoriteButton.setTooltip(new GuiTooltip().setI18nText("replaymod.gui.center.downloadrequired"));
+ }
// Similar for like/dislike buttons
likeButton.setEnabled(selected.downloaded);
dislikeButton.setEnabled(selected.downloaded);
+ if (likeButton.isEnabled()) {
+ likeButton.setTooltip(null);
+ dislikeButton.setTooltip(null);
+ } else {
+ likeButton.setTooltip(new GuiTooltip().setI18nText("replaymod.gui.center.downloadrequired"));
+ dislikeButton.setTooltip(new GuiTooltip().setI18nText("replaymod.gui.center.downloadrequired"));
+ }
likeButton.setI18nLabel("replaymod.gui." + (liked ? "removelike" : "like"));
dislikeButton.setI18nLabel("replaymod.gui." + (disliked ? "removedislike" : "dislike"));
}
diff --git a/src/main/java/com/replaymod/online/gui/GuiUploadReplay.java b/src/main/java/com/replaymod/online/gui/GuiUploadReplay.java
index 6c53ef47..4421df29 100644
--- a/src/main/java/com/replaymod/online/gui/GuiUploadReplay.java
+++ b/src/main/java/com/replaymod/online/gui/GuiUploadReplay.java
@@ -17,7 +17,12 @@ import com.replaymod.replaystudio.studio.ReplayStudio;
import de.johni0702.minecraft.gui.container.GuiContainer;
import de.johni0702.minecraft.gui.container.GuiPanel;
import de.johni0702.minecraft.gui.container.GuiScreen;
-import de.johni0702.minecraft.gui.element.*;
+import de.johni0702.minecraft.gui.element.GuiButton;
+import de.johni0702.minecraft.gui.element.GuiCheckbox;
+import de.johni0702.minecraft.gui.element.GuiImage;
+import de.johni0702.minecraft.gui.element.GuiLabel;
+import de.johni0702.minecraft.gui.element.GuiTextField;
+import de.johni0702.minecraft.gui.element.GuiTooltip;
import de.johni0702.minecraft.gui.element.advanced.GuiDropdownMenu;
import de.johni0702.minecraft.gui.element.advanced.GuiProgressBar;
import de.johni0702.minecraft.gui.element.advanced.GuiTextArea;
@@ -234,6 +239,8 @@ public class GuiUploadReplay extends GuiScreen {
});
validateInputs();
+ name.onTextChanged(s -> validateInputs());
+ tags.onTextChanged(s -> validateInputs());
}
public void validateInputs() {
diff --git a/src/main/java/com/replaymod/pathing/gui/GuiKeyframeRepository.java b/src/main/java/com/replaymod/pathing/gui/GuiKeyframeRepository.java
index 525ef7e8..42a0032c 100644
--- a/src/main/java/com/replaymod/pathing/gui/GuiKeyframeRepository.java
+++ b/src/main/java/com/replaymod/pathing/gui/GuiKeyframeRepository.java
@@ -4,6 +4,7 @@ import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.SettableFuture;
import com.replaymod.core.ReplayMod;
+import com.replaymod.core.utils.Utils;
import com.replaymod.replay.ReplayModReplay;
import com.replaymod.replaystudio.pathing.PathingRegistry;
import com.replaymod.replaystudio.pathing.path.Path;
@@ -11,7 +12,11 @@ import com.replaymod.replaystudio.pathing.path.Timeline;
import com.replaymod.replaystudio.replay.ReplayFile;
import de.johni0702.minecraft.gui.GuiRenderer;
import de.johni0702.minecraft.gui.RenderInfo;
-import de.johni0702.minecraft.gui.container.*;
+import de.johni0702.minecraft.gui.container.AbstractGuiClickableContainer;
+import de.johni0702.minecraft.gui.container.GuiContainer;
+import de.johni0702.minecraft.gui.container.GuiPanel;
+import de.johni0702.minecraft.gui.container.GuiScreen;
+import de.johni0702.minecraft.gui.container.GuiVerticalList;
import de.johni0702.minecraft.gui.element.GuiButton;
import de.johni0702.minecraft.gui.element.GuiLabel;
import de.johni0702.minecraft.gui.element.GuiTextField;
@@ -41,9 +46,16 @@ public class GuiKeyframeRepository extends GuiScreen implements Closeable {
public final GuiButton overwriteButton = new GuiButton(buttonPanel).onClick(new Runnable() {
@Override
public void run() {
- timelines.put(selectedEntry.name, currentTimeline);
- overwriteButton.setDisabled();
- save();
+ GuiYesNoPopup popup = GuiYesNoPopup.open(GuiKeyframeRepository.this,
+ new GuiLabel().setI18nText("replaymod.gui.keyframerepo.overwrite").setColor(Colors.BLACK)
+ ).setYesI18nLabel("gui.yes").setNoI18nLabel("gui.no");
+ Utils.addCallback(popup.getFuture(), doIt -> {
+ if (doIt) {
+ timelines.put(selectedEntry.name, currentTimeline);
+ overwriteButton.setDisabled();
+ save();
+ }
+ }, Throwable::printStackTrace);
}
}).setSize(75, 20).setI18nLabel("replaymod.gui.overwrite").setDisabled();
public final GuiButton saveAsButton = new GuiButton(buttonPanel).onClick(new Runnable() {
diff --git a/src/main/java/com/replaymod/recording/ReplayModRecording.java b/src/main/java/com/replaymod/recording/ReplayModRecording.java
index 70775777..f0584517 100644
--- a/src/main/java/com/replaymod/recording/ReplayModRecording.java
+++ b/src/main/java/com/replaymod/recording/ReplayModRecording.java
@@ -4,6 +4,8 @@ import com.replaymod.core.ReplayMod;
import com.replaymod.core.utils.Restrictions;
import com.replaymod.recording.handler.ConnectionEventHandler;
import com.replaymod.recording.packet.PacketListener;
+import io.netty.channel.ChannelDuplexHandler;
+import io.netty.channel.ChannelHandler;
import net.minecraftforge.common.MinecraftForge;
import net.minecraft.network.NetworkManager;
import net.minecraftforge.fml.common.Mod;
@@ -54,9 +56,12 @@ public class ReplayModRecording {
EventBus bus = MinecraftForge.EVENT_BUS;
bus.register(connectionEventHandler = new ConnectionEventHandler(logger, core));
- NetworkRegistry.INSTANCE.newSimpleChannel(Restrictions.PLUGIN_CHANNEL);
+ NetworkRegistry.INSTANCE.newChannel(Restrictions.PLUGIN_CHANNEL, new RestrictionsChannelHandler());
}
+ @ChannelHandler.Sharable
+ private static class RestrictionsChannelHandler extends ChannelDuplexHandler {}
+
public void initiateRecording(NetworkManager networkManager) {
connectionEventHandler.onConnectedToServerEvent(networkManager);
}
diff --git a/src/main/java/com/replaymod/replay/ReplayHandler.java b/src/main/java/com/replaymod/replay/ReplayHandler.java
index 8fcb7ebd..cfcfc918 100755
--- a/src/main/java/com/replaymod/replay/ReplayHandler.java
+++ b/src/main/java/com/replaymod/replay/ReplayHandler.java
@@ -209,6 +209,9 @@ public class ReplayHandler {
*/
public void spectateEntity(Entity e) {
CameraEntity cameraEntity = getCameraEntity();
+ if (cameraEntity == null) {
+ return; // Cannot spectate if we have no camera
+ }
if (e == null || e == cameraEntity) {
spectating = null;
e = cameraEntity;
diff --git a/src/main/java/com/replaymod/replay/ReplayModReplay.java b/src/main/java/com/replaymod/replay/ReplayModReplay.java
index 24465548..426d7027 100644
--- a/src/main/java/com/replaymod/replay/ReplayModReplay.java
+++ b/src/main/java/com/replaymod/replay/ReplayModReplay.java
@@ -6,7 +6,11 @@ import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.replaymod.core.ReplayMod;
import com.replaymod.core.utils.ModCompat;
-import com.replaymod.replay.camera.*;
+import com.replaymod.replay.camera.CameraController;
+import com.replaymod.replay.camera.CameraControllerRegistry;
+import com.replaymod.replay.camera.CameraEntity;
+import com.replaymod.replay.camera.ClassicCameraController;
+import com.replaymod.replay.camera.VanillaCameraController;
import com.replaymod.replay.gui.overlay.GuiMarkerTimeline;
import com.replaymod.replay.gui.screen.GuiModCompatWarning;
import com.replaymod.replay.handler.GuiHandler;
@@ -99,6 +103,7 @@ public class ReplayModReplay {
@Override
public void onSuccess(NoGuiScreenshot result) {
try {
+ core.printInfoToChat("replaymod.chat.savingthumb");
replayHandler.getReplayFile().writeThumb(result.getImage());
core.printInfoToChat("replaymod.chat.savedthumb");
} catch (IOException e) {
@@ -109,6 +114,7 @@ public class ReplayModReplay {
@Override
public void onFailure(Throwable t) {
t.printStackTrace();
+ core.printWarningToChat("replaymod.chat.failedthumb");
}
});
}
diff --git a/src/main/java/com/replaymod/replay/handler/GuiHandler.java b/src/main/java/com/replaymod/replay/handler/GuiHandler.java
index ae74da26..25af1b22 100644
--- a/src/main/java/com/replaymod/replay/handler/GuiHandler.java
+++ b/src/main/java/com/replaymod/replay/handler/GuiHandler.java
@@ -13,10 +13,10 @@ import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.List;
public class GuiHandler {
private static final int BUTTON_EXIT_SERVER = 1;
- private static final int BUTTON_RETURN_TO_GAME = 4;
private static final int BUTTON_ACHIEVEMENTS = 5;
private static final int BUTTON_STATS = 6;
private static final int BUTTON_OPEN_TO_LAN = 7;
@@ -46,7 +46,9 @@ public class GuiHandler {
// Pause replay when menu is opened
mod.getReplayHandler().getReplaySender().setReplaySpeed(0);
- for(GuiButton b : new ArrayList<>(event.getButtonList())) {
+ GuiButton achievements = null, stats = null, openToLan = null;
+ List buttonList = event.getButtonList();
+ for(GuiButton b : new ArrayList<>(buttonList)) {
switch (b.id) {
// Replace "Exit Server" button with "Exit Replay" button
case BUTTON_EXIT_SERVER:
@@ -55,15 +57,39 @@ public class GuiHandler {
break;
// Remove "Achievements", "Stats" and "Open to LAN" buttons
case BUTTON_ACHIEVEMENTS:
+ buttonList.remove(achievements = b);
+ break;
case BUTTON_STATS:
+ buttonList.remove(stats = b);
+ break;
case BUTTON_OPEN_TO_LAN:
- event.getButtonList().remove(b);
- }
- // Move all buttons except the "Return to game" button upwards
- if (b.id != BUTTON_RETURN_TO_GAME) {
- b.yPosition -= 48;
+ buttonList.remove(openToLan = b);
+ break;
}
}
+ if (achievements != null && stats != null) {
+ moveAllButtonsDirectlyBelowUpwards(buttonList, achievements.yPosition,
+ achievements.xPosition, stats.xPosition + stats.width);
+ }
+ if (openToLan != null) {
+ moveAllButtonsDirectlyBelowUpwards(buttonList, openToLan.yPosition,
+ openToLan.xPosition, openToLan.xPosition + openToLan.width);
+ }
+ }
+ }
+
+ /**
+ * Moves all buttons that are within a rectangle below a certain y coordinate upwards by 24 units.
+ * @param buttons List of buttons
+ * @param belowY The Y limit
+ * @param xStart Left x limit of the rectangle
+ * @param xEnd Right x limit of the rectangle
+ */
+ private void moveAllButtonsDirectlyBelowUpwards(List buttons, int belowY, int xStart, int xEnd) {
+ for (GuiButton button : buttons) {
+ if (button.yPosition >= belowY && button.xPosition <= xEnd && button.xPosition + button.width >= xStart) {
+ button.yPosition -= 24;
+ }
}
}
diff --git a/src/main/java/com/replaymod/simplepathing/ReplayModSimplePathing.java b/src/main/java/com/replaymod/simplepathing/ReplayModSimplePathing.java
index 95596baf..50f646e0 100644
--- a/src/main/java/com/replaymod/simplepathing/ReplayModSimplePathing.java
+++ b/src/main/java/com/replaymod/simplepathing/ReplayModSimplePathing.java
@@ -14,6 +14,7 @@ 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 org.lwjgl.input.Keyboard;
@Mod(modid = ReplayModSimplePathing.MOD_ID,
version = "@MOD_VERSION@",
@@ -22,6 +23,9 @@ import org.apache.logging.log4j.Logger;
public class ReplayModSimplePathing {
public static final String MOD_ID = "replaymod-simplepathing";
+ @Mod.Instance(MOD_ID)
+ public static ReplayModSimplePathing instance;
+
private ReplayMod core;
public static Logger LOGGER;
@@ -39,11 +43,24 @@ public class ReplayModSimplePathing {
PathPreview pathPreview = new PathPreview(this);
pathPreview.register();
+
+ core.getKeyBindingRegistry().registerKeyBinding("replaymod.input.keyframerepository", Keyboard.KEY_X, () -> {
+ if (guiPathing != null) guiPathing.keyframeRepoButtonPressed();
+ });
+ core.getKeyBindingRegistry().registerKeyBinding("replaymod.input.clearkeyframes", Keyboard.KEY_C, () -> {
+ if (guiPathing != null) guiPathing.clearKeyframesButtonPressed();
+ });
+ core.getKeyBindingRegistry().registerRepeatedKeyBinding("replaymod.input.synctimeline", Keyboard.KEY_V, () -> {
+ if (guiPathing != null) guiPathing.syncTimeButtonPressed();
+ });
+ core.getKeyBindingRegistry().registerRaw(Keyboard.KEY_DELETE, () -> {
+ if (guiPathing != null) guiPathing.deleteButtonPressed();
+ });
}
@SubscribeEvent
public void postReplayOpen(ReplayOpenEvent.Post event) {
- currentTimeline = new SPTimeline();
+ clearCurrentTimeline();
guiPathing = new GuiPathing(core, this, event.getReplayHandler());
}
@@ -82,11 +99,11 @@ public class ReplayModSimplePathing {
public void setCurrentTimeline(SPTimeline newTimeline) {
selectedPath = null;
currentTimeline = newTimeline;
+ updateDefaultInterpolatorType();
}
public void clearCurrentTimeline() {
setCurrentTimeline(new SPTimeline());
- updateDefaultInterpolatorType();
}
public SPTimeline getCurrentTimeline() {
diff --git a/src/main/java/com/replaymod/simplepathing/SPTimeline.java b/src/main/java/com/replaymod/simplepathing/SPTimeline.java
index 22a04fbe..a41c6079 100644
--- a/src/main/java/com/replaymod/simplepathing/SPTimeline.java
+++ b/src/main/java/com/replaymod/simplepathing/SPTimeline.java
@@ -71,7 +71,7 @@ public class SPTimeline implements PathingRegistry {
@Getter
private EntityPositionTracker entityTracker;
- private InterpolatorType defaultInterpolatorType = InterpolatorType.fromString("invalid string returns default");
+ private InterpolatorType defaultInterpolatorType;
public SPTimeline() {
this(createInitialTimeline());
@@ -340,13 +340,15 @@ public class SPTimeline implements PathingRegistry {
Optional firstInterpolator =
path.getSegments().stream().findFirst().map(PathSegment::getInterpolator);
- // The interpolator of the previous segment
- Optional interpolatorBefore =
- path.getSegments().stream().filter(s -> s.getEndKeyframe() == keyframe).findFirst().map(PathSegment::getInterpolator);
-
- // The interpolator of the following segment
- Optional interpolatorAfter =
- path.getSegments().stream().filter(s -> s.getStartKeyframe() == keyframe).findFirst().map(PathSegment::getInterpolator);
+ // The interpolator that will be lost once we remove the old keyframe and has to be restored afterwards
+ Optional lostInterpolator = path.getSegments().stream().filter(s -> {
+ // If this is the last keyframe,
+ if (Iterables.getLast(path.getKeyframes()) == keyframe) {
+ return s.getEndKeyframe() == keyframe; // the previous interpolator will be lost
+ } else { // otherwise
+ return s.getStartKeyframe() == keyframe; // the following interpolator will be lost
+ }
+ }).findFirst().map(PathSegment::getInterpolator);
// First remove the old keyframe
Change removeChange = create(path, keyframe);
@@ -369,18 +371,27 @@ public class SPTimeline implements PathingRegistry {
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.flatMap(interpolator ->
+ restoreInterpolatorChange = lostInterpolator.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.flatMap(interpolator ->
- path.getSegments().stream().filter(s -> s.getEndKeyframe() == newKf).findFirst().map(segment ->
- SetInterpolator.create(segment, interpolator)
- )
- ).orElseGet(CombinedChange::create);
+ restoreInterpolatorChange = path.getSegments().stream().filter(s -> s.getEndKeyframe() == newKf)
+ .findFirst().flatMap(segment -> lostInterpolator.map(interpolator -> {
+ // additionally, if the interpolation of this keyframe was set to explicit, that property
+ // has to be transferred to the start keyframe of the new segment
+ if (newKf.getValue(ExplicitInterpolationProperty.PROPERTY).isPresent()) {
+ return CombinedChange.create(
+ SetInterpolator.create(segment, interpolator),
+ UpdateKeyframeProperties.create(path, segment.getStartKeyframe())
+ .setValue(ExplicitInterpolationProperty.PROPERTY, ObjectUtils.NULL).done()
+ );
+ } else {
+ return SetInterpolator.create(segment, interpolator);
+ }
+ })).orElseGet(CombinedChange::create);
}
restoreInterpolatorChange.apply(timeline);
diff --git a/src/main/java/com/replaymod/simplepathing/gui/GuiEditKeyframe.java b/src/main/java/com/replaymod/simplepathing/gui/GuiEditKeyframe.java
index 7fa41072..1b9e8bc0 100644
--- a/src/main/java/com/replaymod/simplepathing/gui/GuiEditKeyframe.java
+++ b/src/main/java/com/replaymod/simplepathing/gui/GuiEditKeyframe.java
@@ -109,6 +109,9 @@ public abstract class GuiEditKeyframe> extends Abst
if (newTime != time) {
change = CombinedChange.createFromApplied(change,
gui.getMod().getCurrentTimeline().moveKeyframe(path, time, newTime));
+ if (gui.getMod().getSelectedPath() == path && gui.getMod().getSelectedTime() == time) {
+ gui.getMod().setSelected(path, newTime);
+ }
}
gui.getMod().getCurrentTimeline().getTimeline().pushChange(change);
close();
@@ -252,6 +255,10 @@ public abstract class GuiEditKeyframe> extends Abst
xField.getDouble(), yField.getDouble(), zField.getDouble(),
yawField.getFloat(), pitchField.getFloat(), rollField.getFloat()
);
+ if (interpolationPanel.getSettingsPanel() == null) {
+ // The last keyframe doesn't have interpolator settings because there is no segment following it
+ return positionChange;
+ }
Interpolator interpolator = interpolationPanel.getSettingsPanel().createInterpolator();
if (interpolationPanel.getInterpolatorType() == InterpolatorType.DEFAULT) {
return CombinedChange.createFromApplied(positionChange, timeline.setInterpolatorToDefault(time),
@@ -303,6 +310,7 @@ public abstract class GuiEditKeyframe> extends Abst
dropdown.setSelected(type); // trigger the callback once to display settings panel
} else {
setSettingsPanel(InterpolatorType.DEFAULT);
+ type = InterpolatorType.DEFAULT;
}
if (getInterpolatorTypeNoDefault(type).getInterpolatorClass().isInstance(interpolator)) {
//noinspection unchecked
diff --git a/src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java b/src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java
index bde36719..77854930 100644
--- a/src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java
+++ b/src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java
@@ -1,10 +1,12 @@
package com.replaymod.simplepathing.gui;
+import com.google.common.base.Preconditions;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import com.replaymod.core.ReplayMod;
+import com.replaymod.core.utils.Utils;
import com.replaymod.pathing.gui.GuiKeyframeRepository;
import com.replaymod.pathing.player.RealtimeTimelinePlayer;
import com.replaymod.pathing.properties.CameraProperties;
@@ -255,6 +257,7 @@ public class GuiPathing {
}
};
+ private final ReplayMod core;
private final ReplayModSimplePathing mod;
private final ReplayHandler replayHandler;
private final RealtimeTimelinePlayer player;
@@ -264,6 +267,7 @@ public class GuiPathing {
private SettableFuture entityTrackerFuture;
public GuiPathing(final ReplayMod core, final ReplayModSimplePathing mod, final ReplayHandler replayHandler) {
+ this.core = core;
this.mod = mod;
this.replayHandler = replayHandler;
this.player = new RealtimeTimelinePlayer(replayHandler);
@@ -300,9 +304,15 @@ public class GuiPathing {
ListenableFuture future = player.start(mod.getCurrentTimeline().getTimeline(), startTime);
overlay.setCloseable(false);
overlay.setMouseVisible(true);
+ core.printInfoToChat("replaymod.chat.pathstarted");
Futures.addCallback(future, new FutureCallback() {
@Override
public void onSuccess(@Nullable Void result) {
+ if (future.isCancelled()) {
+ core.printInfoToChat("replaymod.chat.pathinterrupted");
+ } else {
+ core.printInfoToChat("replaymod.chat.pathfinished");
+ }
overlay.setCloseable(true);
timePath.setActive(true);
}
@@ -393,94 +403,90 @@ public class GuiPathing {
}
});
- core.getKeyBindingRegistry().registerKeyBinding("replaymod.input.keyframerepository", Keyboard.KEY_X, new Runnable() {
- @Override
- public void run() {
- if (!overlay.isVisible()) {
- return;
- }
- try {
- GuiKeyframeRepository gui = new GuiKeyframeRepository(
- mod.getCurrentTimeline(), replayHandler.getReplayFile(), mod.getCurrentTimeline().getTimeline());
- Futures.addCallback(gui.getFuture(), new FutureCallback() {
- @Override
- public void onSuccess(Timeline result) {
- if (result != null) {
- mod.setCurrentTimeline(new SPTimeline(result));
- }
- }
+ startLoadingEntityTracker();
+ }
- @Override
- public void onFailure(Throwable t) {
- t.printStackTrace();
- core.printWarningToChat("Error loading timeline: " + t.getMessage());
- }
- });
- gui.display();
- } catch (IOException e) {
- e.printStackTrace();
- core.printWarningToChat("Error loading timeline: " + e.getMessage());
- }
- }
- });
-
- core.getKeyBindingRegistry().registerKeyBinding("replaymod.input.clearkeyframes", Keyboard.KEY_C, () -> {
- GuiYesNoPopup popup = GuiYesNoPopup.open(overlay,
- new GuiLabel().setI18nText("replaymod.gui.clearcallback.title").setColor(Colors.BLACK)
- ).setYesI18nLabel("gui.yes").setNoI18nLabel("gui.no");
- Futures.addCallback(popup.getFuture(), new FutureCallback() {
+ public void keyframeRepoButtonPressed() {
+ try {
+ GuiKeyframeRepository gui = new GuiKeyframeRepository(
+ mod.getCurrentTimeline(), replayHandler.getReplayFile(), mod.getCurrentTimeline().getTimeline());
+ Futures.addCallback(gui.getFuture(), new FutureCallback() {
@Override
- public void onSuccess(Boolean delete) {
- if (delete) {
- mod.clearCurrentTimeline();
- if (entityTracker != null) {
- mod.getCurrentTimeline().setEntityTracker(entityTracker);
- }
+ public void onSuccess(Timeline result) {
+ if (result != null) {
+ mod.setCurrentTimeline(new SPTimeline(result));
}
}
@Override
public void onFailure(Throwable t) {
t.printStackTrace();
+ core.printWarningToChat("Error loading timeline: " + t.getMessage());
}
});
- });
+ gui.display();
+ } catch (IOException e) {
+ e.printStackTrace();
+ core.printWarningToChat("Error loading timeline: " + e.getMessage());
+ }
+ }
- core.getKeyBindingRegistry().registerRepeatedKeyBinding("replaymod.input.synctimeline", Keyboard.KEY_V, () -> {
- // Current replay time
- int time = replayHandler.getReplaySender().currentTimeStamp();
- // Position of the cursor
- int cursor = timeline.getCursorPosition();
- // Get the last time keyframe before the cursor
- 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();
- // Replay time at the keyframe
- // This is a keyframe from the time path, so it _should_ always have a time property
- int keyframeTime = keyframe.getValue(TimestampProperty.PROPERTY).get();
- // Replay time passed
- int timePassed = time - keyframeTime;
- // Speed (set to 1 when shift is held)
- double speed = Keyboard.isKeyDown(Keyboard.KEY_LSHIFT) ? 1 : overlay.getSpeedSliderValue();
- // Cursor time passed
- int cursorPassed = (int) (timePassed / speed);
- // Move cursor to new position
- timeline.setCursorPosition(keyframeCursor + cursorPassed);
- // Deselect keyframe to allow the user to add a new one right away
- mod.setSelected(null, 0);
- });
- });
-
- core.getKeyBindingRegistry().registerRaw(Keyboard.KEY_DELETE, () -> {
- if (!overlay.isVisible()) {
- return;
+ public void clearKeyframesButtonPressed() {
+ GuiYesNoPopup popup = GuiYesNoPopup.open(replayHandler.getOverlay(),
+ new GuiLabel().setI18nText("replaymod.gui.clearcallback.title").setColor(Colors.BLACK)
+ ).setYesI18nLabel("gui.yes").setNoI18nLabel("gui.no");
+ Futures.addCallback(popup.getFuture(), new FutureCallback() {
+ @Override
+ public void onSuccess(Boolean delete) {
+ if (delete) {
+ mod.clearCurrentTimeline();
+ if (entityTracker != null) {
+ mod.getCurrentTimeline().setEntityTracker(entityTracker);
+ }
+ }
}
- if (mod.getSelectedPath() != null) {
- updateKeyframe(mod.getSelectedPath());
+
+ @Override
+ public void onFailure(Throwable t) {
+ t.printStackTrace();
}
});
+ }
+ public void syncTimeButtonPressed() {
+ // Current replay time
+ int time = replayHandler.getReplaySender().currentTimeStamp();
+ // Position of the cursor
+ int cursor = timeline.getCursorPosition();
+ // Get the last time keyframe before the cursor
+ 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();
+ // Replay time at the keyframe
+ // This is a keyframe from the time path, so it _should_ always have a time property
+ int keyframeTime = keyframe.getValue(TimestampProperty.PROPERTY).get();
+ // Replay time passed
+ int timePassed = time - keyframeTime;
+ // Speed (set to 1 when shift is held)
+ double speed = Keyboard.isKeyDown(Keyboard.KEY_LSHIFT) ? 1 : replayHandler.getOverlay().getSpeedSliderValue();
+ // Cursor time passed
+ int cursorPassed = (int) (timePassed / speed);
+ // Move cursor to new position
+ timeline.setCursorPosition(keyframeCursor + cursorPassed);
+ // Deselect keyframe to allow the user to add a new one right away
+ mod.setSelected(null, 0);
+ });
+ }
+
+ public void deleteButtonPressed() {
+ if (mod.getSelectedPath() != null) {
+ updateKeyframe(mod.getSelectedPath());
+ }
+ }
+
+ private void startLoadingEntityTracker() {
+ Preconditions.checkState(entityTrackerFuture == null);
// Start loading entity tracker
entityTrackerFuture = SettableFuture.create();
new Thread(() -> {
@@ -493,12 +499,13 @@ public class GuiPathing {
}
});
logger.info("Loaded entity tracker in " + (System.currentTimeMillis() - start) + "ms");
- } catch (IOException e) {
+ } catch (Throwable e) {
logger.error("Loading entity tracker:", e);
mod.getCore().runLater(() -> {
mod.getCore().printWarningToChat("Error loading entity tracker: %s", e.getLocalizedMessage());
entityTrackerFuture.setException(e);
});
+ return;
}
entityTracker = tracker;
mod.getCore().runLater(() -> {
@@ -557,7 +564,9 @@ public class GuiPathing {
@Override
public void onFailure(@Nonnull Throwable t) {
- popup.close();
+ String message = "Failed to load entity tracker, pathing will be unavailable.";
+ GuiReplayOverlay overlay = replayHandler.getOverlay();
+ Utils.error(LOGGER, overlay, CrashReport.makeCrashReport(t, message), popup::close);
}
});
return false;
diff --git a/src/main/resources/assets/replaymod/lang b/src/main/resources/assets/replaymod/lang
new file mode 160000
index 00000000..da6f6f8d
--- /dev/null
+++ b/src/main/resources/assets/replaymod/lang
@@ -0,0 +1 @@
+Subproject commit da6f6f8d123cbda3d04e027948e29161ce302bdc
diff --git a/src/main/resources/assets/replaymod/lang/en_US.lang b/src/main/resources/assets/replaymod/lang/en_US.lang
deleted file mode 100644
index c901032b..00000000
--- a/src/main/resources/assets/replaymod/lang/en_US.lang
+++ /dev/null
@@ -1,539 +0,0 @@
-replaymod.title=Replay Mod
-
-#Website API translations
-replaymod.api.parammissing=Required parameter missing: %1$s
-replaymod.api.invalidvalue=Invalid value passed for parameter %1$s - Possible values: %2$s
-replaymod.api.invalidlogin=Invalid login data provided
-replaymod.api.invalidauthkey=Invalid authentication key provided
-replaymod.api.notenoughspace=Not enough space left to upload this file. Consider purchasing Premium on ReplayMod.com
-replaymod.api.invalidfile=This file is invalid
-replaymod.api.fileduplicate=This file has already been uploaded
-replaymod.api.filenoexist=This file does not exist
-replaymod.api.nopermissions=You don't have permission to access this file
-replaymod.api.alreadyrated=You already rated this file
-replaymod.api.usernoexist=This user does not exist
-replaymod.api.invalidcategory=Invalid file category
-replaymod.api.nofilesuploaded=No files uploaded
-replaymod.api.toomanyvalues=Too many values passed for %1$s
-replaymod.api.noparameters=At least one parameter required. Parameters: %1$s
-replaymod.api.invalidfilename=File name must be between 5 and 30 characters long
-replaymod.api.unknownlang=This language has not been translated
-replaymod.api.zippingerror=An error occured while zipping the language files
-replaymod.api.usernameexists=This username is already taken
-replaymod.api.mailexists=This Email address is already linked to an account
-replaymod.api.invalidmail=Invalid Email Address
-replaymod.api.nopermissions=You don't have permission to do this
-replaymod.api.authfailed=Authentication to the Minecraft Session Servers failed
-replaymod.api.mcuserexists=A ReplayMod account is already associated with this Minecraft account
-replaymod.api.filetoobig=The uploaded file is too big
-replaymod.api.usernametoolong=The username is too long
-replaymod.api.descriptiontoolong=The description is too long
-replaymod.api.passwordlength=The password has to be between 5 and 1024 characters long
-replaymod.api.suspended=Your account has been suspended
-
-#All of the chat messages
-replaymod.chat.recordingstarted=Recording started
-replaymod.chat.recordingfailed=Failed to start recording
-
-replaymod.chat.savingthumb=Saving Thumbnail...
-replaymod.chat.savedthumb=Thumbnail has been successfully saved
-replaymod.chat.failedthumb=Thumbnail could not be saved
-
-replaymod.chat.addedmarker=Event Marker has been added
-
-#Chat messages displayed in Replay Viewer
-replaymod.chat.morekeyframes=At least 2 position keyframes and 2 time keyframes required
-replaymod.chat.negativetime=Camera Paths can't play time backwards. Please consult your nearest time lord for assistance.
-replaymod.chat.pathstarted=Camera Path started
-replaymod.chat.pathfinished=Camera Path finished
-replaymod.chat.pathinterrupted=Camera Path canceled
-
-#Replay Categories
-replaymod.category=Category
-replaymod.category.survival=Survival
-replaymod.category.minigame=Minigame
-replaymod.category.build=Build
-replaymod.category.misc=Miscellaneous
-
-#Common GUI-related strings
-replaymod.gui.replay=Replay
-replaymod.gui.login=Login
-replaymod.gui.logout=Logout
-replaymod.gui.cancel=Cancel
-replaymod.gui.username=Username
-replaymod.gui.password=Password
-replaymod.gui.back=Back
-replaymod.gui.duration=Duration
-replaymod.gui.load=Load
-replaymod.gui.download=Download
-replaymod.gui.like=Like
-replaymod.gui.dislike=Dislike
-replaymod.gui.removelike=Remove Like
-replaymod.gui.removedislike=Remove Dislike
-replaymod.gui.save=Save
-replaymod.gui.upload=Upload
-replaymod.gui.rename=Rename
-replaymod.gui.remove=Remove
-replaymod.gui.add=Add
-replaymod.gui.start=Start
-replaymod.gui.end=End
-replaymod.gui.delete=Delete
-replaymod.gui.recording=Recording
-replaymod.gui.speed=Speed
-replaymod.gui.register=Register
-replaymod.gui.mail=Email Address
-replaymod.gui.loading=Loading
-replaymod.gui.pleasewait=Please wait
-replaymod.gui.render=Render
-replaymod.gui.iphidden=Server IP Hidden
-replaymod.gui.overwrite=Overwrite
-replaymod.gui.saveas=Save as ...
-replaymod.gui.done=Done
-replaymod.gui.close=Close
-replaymod.gui.notagain=Don't show again
-
-replaymod.gui.renderdonetitle=Video rendered
-replaymod.gui.openfolder=Open Video Folder
-replaymod.gui.youtubeupload=Upload to YouTube
-replaymod.gui.renderdone1=Your video was successfully rendered.
-replaymod.gui.renderdone2=How would you like to proceed?
-replaymod.gui.videotitle=Title
-replaymod.gui.videodescription=Description
-replaymod.gui.videotags=Tags,Tags,Tags
-replaymod.gui.videothumbnail=Video Thumbnail
-replaymod.gui.videovisibility.private=Private
-replaymod.gui.videovisibility.unlisted=Unlisted
-replaymod.gui.videovisibility.public=Public
-replaymod.gui.ytuploadprogress.auth=[1/4] Authorization
-replaymod.gui.ytuploadprogress.prepare_video=[2/4] Preparing video: %%d%%%%
-replaymod.gui.ytuploadprogress.upload=[3/4] Uploading: %%d%%%%
-replaymod.gui.ytuploadprogress.cleanup=[4/4] Cleanup
-replaymod.gui.ytuploadprogress.done=Done: %s
-replaymod.gui.titleempty=Title cannot be empty
-replaymod.gui.videothumbnailtoolarge=Thumbnail size exceeds 2MB
-replaymod.gui.videothumbnailformat=Thumbnail has to be either JPEG or PNG format
-
-replaymod.gui.original=Original
-replaymod.gui.modified=Modified
-
-replaymod.gui.outdated=There is a newer Replay Mod version available. Please download it from replaymod.com
-
-replaymod.gui.hours=h
-replaymod.gui.minutes=min
-replaymod.gui.seconds=sec
-replaymod.gui.milliseconds=ms
-
-replaymod.gui.pitch=Pitch
-replaymod.gui.yaw=Yaw
-replaymod.gui.roll=Roll
-
-replaymod.gui.camera=Camera
-replaymod.gui.position=Position
-
-replaymod.gui.unknownerror=An unknown error occured
-
-replaymod.gui.exit=Exit Replay
-replaymod.gui.java=Java 1.7 or newer required
-replaymod.gui.morereplays=At least one Replay required
-
-replaymod.gui.loggedin=LOGGED IN
-replaymod.gui.loggedout=LOGGED OUT
-
-replaymod.gui.mainmenu=Main Menu
-replaymod.gui.settings=Settings
-
-replaymod.gui.keyframerepo.delete=Are you sure you want to delete these keyframes?
-
-replaymod.gui.restorereplay1=It seems like Minecraft has not quit normally.
-replaymod.gui.restorereplay2=The Replay "%1$s" was not saved correctly.
-replaymod.gui.restorereplay3=Do you wish to recover it?
-
-replaymod.gui.loadentitytracker=Loading entity positions: %%d%%%%
-
-#Only change these if it's neccessary
-replaymod.gui.replayviewer=Replay Viewer
-replaymod.gui.replaycenter=Replay Center
-replaymod.gui.replaysettings=Replay Settings
-replaymod.gui.replayeditor=Replay Editor
-
-#Login GUI
-replaymod.gui.login.title=Login to ReplayMod.com
-replaymod.gui.login.logging=Logging in...
-replaymod.gui.login.incorrect=Incorrect username or password
-replaymod.gui.login.connectionerror=Could not connect to ReplayMod.com
-
-replaymod.gui.login.skip=Skip
-
-replaymod.gui.register.title=Register on ReplayMod.com
-replaymod.gui.register.confirmpw=Confirm Password
-
-replaymod.gui.register.disclaimer=By registering an account, you agree to the Replay Mod's Terms of Service: https://replaymod.com/legal/terms
-
-replaymod.gui.register.error.nomatch=Passwords don't match
-replaymod.gui.register.error.shortusername=Username has to be at least 5 characters long
-replaymod.gui.register.error.invalidname=Username may only contain letters and numbers
-replaymod.gui.register.error.shortpw=Password has to be at least 5 characters long
-replaymod.gui.register.error.longpw=Password has to be at most 1024 characters long
-replaymod.gui.register.error.authfailed=Could not authenticate your Minecraft account
-
-#Replay Viewer GUI
-replaymod.gui.viewer.rename.title=Rename Replay
-replaymod.gui.viewer.rename.name=Replay Name
-replaymod.gui.viewer.replayfolder=Open Replay Folder...
-replaymod.gui.viewer.noauth=Log in to upload a Replay
-replaymod.gui.viewer.alreadyuploaded=This Replay has already been uploaded
-
-replaymod.gui.viewer.delete.linea=Are you sure you want to delete this replay?
-replaymod.gui.viewer.delete.failed1=Your OS did not allow us to move the replay. There is
-replaymod.gui.viewer.delete.failed2=nothing we can do about this. You have to do it yourself.
-replaymod.gui.viewer.delete.lineb='%1$s' will be lost forever! (a long time!)
-
-replaymod.gui.viewer.download.title=Downloading Replay File...
-replaymod.gui.viewer.download.message=Please wait while "%1$s" is being downloaded.
-
-replaymod.gui.viewer.chooser.title=Choose Replay File
-replaymod.gui.viewer.chooser.message=You have a modified version of "%1$s" on your computer. Which Replay do you want to load?
-
-replaymod.gui.login.noacc=Don't have an account yet?
-
-#Replay Center GUI
-replaymod.gui.center.logoutcallback=Do you really want to log out?
-replaymod.gui.center.top.recent=Recent
-replaymod.gui.center.top.best=Best
-replaymod.gui.center.top.downloaded=Downloaded
-replaymod.gui.center.top.favorited=Favorited
-replaymod.gui.center.top.search=Search
-
-replaymod.gui.center.search.filters=Search Filters
-replaymod.gui.center.search.gametype=Gametype
-replaymod.gui.center.search.order=Sort by
-replaymod.gui.center.search.order.best=Best
-replaymod.gui.center.search.order.recent=Recent
-replaymod.gui.center.search.name=Replay Name
-replaymod.gui.center.search.server=Server IP (Multiplayer only)
-replaymod.gui.center.search.category=Filter by Category
-replaymod.gui.center.search.version=Filter by Version
-
-replaymod.gui.center.downloadrequired=Download the Replay to rate or favorite it
-
-replaymod.gui.center.favorite=Favorite
-replaymod.gui.center.unfavorite=Unfavorite
-replaymod.gui.center.favorites=Favorites
-
-replaymod.gui.center.author=by %1$s%2$s
-
-#Upload GUI
-replaymod.gui.upload.title=Upload File
-replaymod.gui.upload.start=Start Upload
-replaymod.gui.upload.cancel=Cancel Upload
-replaymod.gui.upload.tagshint=Tags separated by comma
-replaymod.gui.upload.namehint=Replay Title
-replaymod.gui.upload.descriptionhint=Description
-replaymod.gui.upload.duration=Duration: %02dm%02ds
-replaymod.gui.upload.uploading=Uploading...
-replaymod.gui.upload.success=File has been successfully uploaded
-replaymod.gui.upload.canceled=Upload has been canceled
-replaymod.gui.upload.hideip2=Hide Server IP (%s)
-replaymod.gui.upload.nothumbnail=Missing Thumbnail. Please create a Thumbnail in the Replay using the %1$s key.
-replaymod.gui.upload.tryagain=Try again?
-
-replaymod.gui.upload.error.name.length=The Replay Name has to be between 5 and 30 characters long
-replaymod.gui.upload.error.name=The Replay Name may not contain special characters
-replaymod.gui.upload.error.tags=Tags may only contain letters and are separated using a comma
-
-#Replay Editor
-replaymod.gui.editor.replayfile=Replay File
-
-replaymod.gui.editor.savemode.override=Replace Source File
-replaymod.gui.editor.savemode.newfile=Save to new File
-
-replaymod.gui.editor.trim.title=Trim Replay
-replaymod.gui.editor.connect.title=Connect Replays
-replaymod.gui.editor.modify.title=Modify Replay
-
-replaymod.gui.editor.trim.description=Removes the beginning and end of a Replay File, only keeping the Replay between the given timestamps
-replaymod.gui.editor.connect.description=Connects multiple Replays in the specified order
-replaymod.gui.editor.modify.description=Provides several filters to modify Replay Files
-
-replaymod.gui.editor.trim.marker=Select Event Marker
-
-replaymod.gui.editor.progress.title=Editing Replay File...
-replaymod.gui.editor.progress.pleasewait=Please wait while the Replay is being edited.
-
-replaymod.gui.editor.progress.status.initializing=Initializing
-replaymod.gui.editor.progress.status.writing.raw=Rewriting Replay...
-replaymod.gui.editor.progress.status.writing.final=Writing File to disk...
-replaymod.gui.editor.progress.status.finished=Finished Editing!
-
-replaymod.gui.editor.disclaimer=The Replay Editor is an experimental feature and may contain bugs.
-
-#Cancel Replay GUI
-replaymod.gui.cancelrender.title=Cancel Rendering
-replaymod.gui.cancelrender.message=Are you sure that you want to cancel the current rendering process?
-#Saving Replay GUI
-replaymod.gui.replaysaving.title=Saving Replay File...
-replaymod.gui.replaysaving.message=Please wait while your recent Replay is being saved.
-replaymod.gui.replaymodified.message=Replay was modified. Would you like to save the changes?
-replaymod.gui.replaymodified.yes=Save Modified Replay
-replaymod.gui.replaymodified.no=Discard Changes
-replaymod.gui.replaymodified.warning1=A Replay named "%1$s" already exists.
-replaymod.gui.replaymodified.warning2=Are you sure you want to replace it?
-
-#Player Overview GUI
-replaymod.gui.playeroverview.visible=Visible
-replaymod.gui.playeroverview.hideall=Hide all
-replaymod.gui.playeroverview.showall=Show all
-replaymod.gui.playeroverview.spectate=Spectate Player
-replaymod.gui.playeroverview.remembersettings=Remember Hidden Players
-replaymod.gui.playeroverview.remembersettings.description=Saves Player Visibility in Replay File
-
-#Replay Mod Settings GUI
-replaymod.gui.settings.title=Replay Mod Settings
-
-replaymod.gui.settings.interpolation.linear=Linear
-replaymod.gui.settings.interpolation.cubic=Cubic
-replaymod.gui.settings.interpolation.catmullrom=Catmull Rom
-
-replaymod.gui.settings.bitrate=Video Bitrate
-replaymod.gui.settings.framerate=Video Framerate
-replaymod.gui.settings.notifications=Enable Notifications
-replaymod.gui.settings.recordserver=Record Server
-replaymod.gui.settings.recordsingleplayer=Record Singleplayer
-replaymod.gui.settings.indicator=Recording Indicator
-replaymod.gui.settings.lighting=Ambient Lighting
-replaymod.gui.settings.forcechunks=Force Render Chunks
-replaymod.gui.settings.resources=Server Resource Packs
-replaymod.gui.settings.interpolation=Path Interpolation
-replaymod.gui.settings.linearinterpolation=Linear Path Interpolation
-replaymod.gui.settings.pathpreview=Show Path Preview
-replaymod.gui.settings.keyframecleancallback=Clear Confirmation
-replaymod.gui.settings.renderinvisible=Render invisible Entities
-replaymod.gui.settings.camera=Camera
-replaymod.gui.settings.showchat=Show Chat
-replaymod.gui.settings.interpolator=Default Interpolator
-
-replaymod.gui.settings.warning.linea=WARNING: Recording settings will be
-replaymod.gui.settings.warning.lineb=applied the next time you join a world.
-
-#Replay Mod Keybindings
-replaymod.input.lighting=Toggle Lighting
-replaymod.input.thumbnail=Capture Thumbnail
-replaymod.input.playeroverview=Player Overview
-replaymod.input.clearkeyframes=Clear Keyframes
-replaymod.input.synctimeline=Synchronize Timeline
-replaymod.input.keyframerepository=Open Keyframe Presets
-replaymod.input.rollclockwise=Roll Clockwise
-replaymod.input.rollcounterclockwise=Roll Counterclockwise
-replaymod.input.resettilt=Reset Camera Tilt
-replaymod.input.playpause=Play/Pause Replay
-replaymod.input.marker=Add Event Marker
-replaymod.input.pathpreview=Toggle Path Preview
-replaymod.input.assetmanager=Asset Manager
-replaymod.input.objectmanager=Object Manager
-replaymod.input.interpolation=Toggle Interpolation
-replaymod.input.settings=ReplayMod Settings
-
-# CameraControllers
-replaymod.camera.classic=Classic
-replaymod.camera.vanilla=Vanilla-ish
-
-#Keyframe Presets GUI
-replaymod.gui.keyframerepository.title=Keyframe Repository
-replaymod.gui.keyframerepository.presets=Keyframe Presets
-replaymod.gui.keyframerepository.positionkeyframes=Position Keyframes
-replaymod.gui.keyframerepository.timekeyframes=Time Keyframes
-
-replaymod.gui.keyframerepository.savecurrent=Save current Path
-
-replaymod.gui.keyframerepository.noentries=No Presets available
-
-replaymod.gui.keyframerepository.preset.defaultname=New Keyframe Preset
-replaymod.gui.keyframerepository.duplicate=This Preset already exists
-
-#Edit Keyframe GUI
-replaymod.gui.editkeyframe.title.pos=Edit Position Keyframe
-replaymod.gui.editkeyframe.title.time=Edit Time Keyframe
-replaymod.gui.editkeyframe.title.marker=Edit Event Marker
-replaymod.gui.editkeyframe.title.spec=Edit Spectator Keyframe
-replaymod.gui.editkeyframe.xpos=X Position
-replaymod.gui.editkeyframe.ypos=Y Position
-replaymod.gui.editkeyframe.zpos=Z Position
-replaymod.gui.editkeyframe.campitch=Camera Pitch
-replaymod.gui.editkeyframe.camyaw=Camera Yaw
-replaymod.gui.editkeyframe.camroll=Camera Roll
-replaymod.gui.editkeyframe.timelineposition=Timeline Position
-replaymod.gui.editkeyframe.markername=Event Marker Name
-replaymod.gui.editkeyframe.timestamp=Time Index
-
-replaymod.gui.editkeyframe.spec.method=Spectating Method
-replaymod.gui.editkeyframe.spec.method.firstperson=First Person
-replaymod.gui.editkeyframe.spec.method.shoulder=Shoulder Camera
-
-replaymod.gui.editkeyframe.spec.method.shoulder.distance=Camera Distance
-replaymod.gui.editkeyframe.spec.method.shoulder.pitch=Pitch Offset
-replaymod.gui.editkeyframe.spec.method.shoulder.yaw=Rotation Angle
-replaymod.gui.editkeyframe.spec.method.shoulder.smoothness=Path Smoothness
-
-replaymod.gui.editkeyframe.interpolator=Interpolator
-replaymod.gui.editkeyframe.interpolator.default.name=Default Interpolator
-replaymod.gui.editkeyframe.interpolator.default.desc=Uses the default interpolator defined in the Replay Mod Settings.
-replaymod.gui.editkeyframe.interpolator.catmullrom.name=Catmull Rom Spline Interpolator
-replaymod.gui.editkeyframe.interpolator.catmullrom.desc=Calculates a catmull rom spline with customizable alpha value for precise tightness and smoothness of the camera path.
-replaymod.gui.editkeyframe.interpolator.catmullrom.alpha=Alpha:
-replaymod.gui.editkeyframe.interpolator.cubic.name=Cubic Spline Interpolator
-replaymod.gui.editkeyframe.interpolator.cubic.desc=Calculates a cubic equation matrix for all points for a smooth camera path.
-replaymod.gui.editkeyframe.interpolator.linear.name=Linear Interpolator
-replaymod.gui.editkeyframe.interpolator.linear.desc=Draws straight lines between the keyframes.
-
-#Render Settings GUI
-replaymod.gui.rendersettings.title=Rendering Options
-replaymod.gui.rendersettings.renderer=Rendering Method
-
-replaymod.gui.rendersettings.renderer.default=Default Rendering
-replaymod.gui.rendersettings.renderer.tiled=Tiled Rendering
-replaymod.gui.rendersettings.renderer.stereoscopic=Stereoscopic Rendering
-replaymod.gui.rendersettings.renderer.cubic=Cubic Rendering
-replaymod.gui.rendersettings.renderer.equirectangular=Equirectangular Rendering
-replaymod.gui.rendersettings.renderer.ods=ODS Rendering
-
-replaymod.gui.rendersettings.renderer.default.description=Renders the video in the specified resolution. Fastest Rendering Option.
-replaymod.gui.rendersettings.renderer.stereoscopic.description=Renders the video as a stereoscopic (side-by-side) 3D movie, usable by different 3D technologies. The image for one eye is half the width of the video
-replaymod.gui.rendersettings.renderer.cubic.description=Renders the video with a 360 degree panoramic view, using Cubic Projection. This is usable by several 360 degree video players (and the Oculus Rift), for example VR Player.
-replaymod.gui.rendersettings.renderer.equirectangular.description=Renders the video with a 360 degree panoramic view, using Equirectangular Projection. This is usable by YouTube's new 360 degree video function, and several video players (and the Oculus Rift), for example VR Player.
-replaymod.gui.rendersettings.renderer.ods.description=Renders the video with a stereoscopic 3D 360 degree panoramic view, also called VR Video. This is usable by YouTube's new VR video function.
-
-replaymod.gui.rendersettings.customresolution=Video Resolution
-replaymod.gui.rendersettings.customresolution.warning.cubic=Cubic Rendering requires an aspect ratio of 4:3
-replaymod.gui.rendersettings.customresolution.warning.equirectangular=Equirectangular Rendering requires an aspect ratio of 2:1
-replaymod.gui.rendersettings.customresolution.warning.yuv420=For this preset, the width and height values need to be even numbers
-replaymod.gui.rendersettings.customresolution.warning.ods=ODS Rendering requires an aspect ratio of 1:1
-replaymod.gui.rendersettings.resolution=Video Resolution
-replaymod.gui.rendersettings.interpolation=Path Interpolation
-replaymod.gui.rendersettings.forcechunks=Force Render Chunks
-replaymod.gui.rendersettings.framerate=Video Framerate
-replaymod.gui.rendersettings.quality=Video Quality
-replaymod.gui.rendersettings.chromakey=Chroma Keying
-replaymod.gui.rendersettings.skycolor=Sky Color
-replaymod.gui.rendersettings.nametags=Render Name Tags
-replaymod.gui.rendersettings.outputfile=Output File
-replaymod.gui.rendersettings.360metadata=Inject 360° Metadata
-replaymod.gui.rendersettings.360metadata.error=360° Metadata can only be added to Equirectangular videos in MP4 format
-
-replaymod.gui.rendersettings.command=Command
-replaymod.gui.rendersettings.arguments=Command Line Arguments
-replaymod.gui.rendersettings.ffmpeg.description=If you are an advanced user, you can customize the command line parameters used to export the video. For more information, check http://replaymod.com/docs
-
-replaymod.gui.rendersettings.stabilizecamera=Stabilize Camera
-replaymod.gui.rendersettings.exportyoutube=Export for YouTube
-
-replaymod.gui.rendersettings.video=Video Settings
-replaymod.gui.rendersettings.advanced=Advanced Settings
-replaymod.gui.rendersettings.commandline=Command Line Settings
-
-replaymod.gui.rendersettings.presets=Encoding Presets
-replaymod.gui.rendersettings.presets.mp4.custom=MP4 - Custom Bitrate
-replaymod.gui.rendersettings.presets.mp4.high=MP4 - High Quality
-replaymod.gui.rendersettings.presets.mp4.default=MP4 - Default Quality
-replaymod.gui.rendersettings.presets.mp4.potato=MP4 - Potato Quality
-replaymod.gui.rendersettings.presets.webm.custom=WEBM - Custom Bitrate
-replaymod.gui.rendersettings.presets.mkv.lossless=MKV - Lossless
-replaymod.gui.rendersettings.presets.png=PNG Sequence
-
-replaymod.gui.rendersettings.antialiasing=Anti-Aliasing
-replaymod.gui.rendersettings.antialiasing.none=None
-replaymod.gui.rendersettings.antialiasing.x2=2x
-replaymod.gui.rendersettings.antialiasing.x4=4x
-replaymod.gui.rendersettings.antialiasing.x8=8x
-
-#Render Queue GUI
-replaymod.gui.renderqueue.title=Render Queue
-replaymod.gui.renderqueue.open=Open Queue
-replaymod.gui.renderqueue.jobname=Job Name:
-replaymod.gui.renderqueue.add=Add current configuration
-
-#Rendering GUI
-replaymod.gui.rendering.title=Rendering Video
-replaymod.gui.rendering.pause=Pause Rendering
-replaymod.gui.rendering.resume=Resume Rendering
-replaymod.gui.rendering.cancel=Cancel Rendering
-replaymod.gui.rendering.cancel.callback=Are you sure?
-replaymod.gui.rendering.preview=Show Preview (Performance might suffer)
-replaymod.gui.rendering.progress=Frames rendered: %1$d / %2$d
-replaymod.gui.rendering.timetaken=Render Time
-replaymod.gui.rendering.timeleft=Time Left
-
-#Render Errors
-replaymod.gui.rendering.error.title=Rendering Failed
-replaymod.gui.rendering.error.optifine=Please uninstall Optifine before rendering.
-replaymod.gui.rendering.error.message=To render a video, you need to have ffmpeg installed. http://ffmpeg.org
-
-#Ingame Menu
-replaymod.gui.ingame.menu.addposkeyframe=Add Position Keyframe
-replaymod.gui.ingame.menu.removeposkeyframe=Remove Position Keyframe
-replaymod.gui.ingame.menu.addspeckeyframe=Add Spectator Keyframe
-replaymod.gui.ingame.menu.removespeckeyframe=Remove Spectator Keyframe
-replaymod.gui.ingame.menu.addtimekeyframe=Add Time Keyframe
-replaymod.gui.ingame.menu.removetimekeyframe=Remove Time Keyframe
-replaymod.gui.ingame.menu.pause=Pause Replay
-replaymod.gui.ingame.menu.unpause=Unpause Replay
-replaymod.gui.ingame.menu.renderpath=Render Camera Path
-replaymod.gui.ingame.menu.playpath=Play Camera Path from Cursor Position
-replaymod.gui.ingame.menu.playpathfromstart=Play Camera Path from Start
-replaymod.gui.ingame.menu.pausepath=Pause Camera Path
-replaymod.gui.ingame.menu.zoomin=Zoom in
-replaymod.gui.ingame.menu.zoomout=Zoom out
-
-replaymod.gui.ingame.unnamedmarker=Unnamed Event Marker
-
-#Clear Keyframe Callback
-replaymod.gui.clearcallback.title=Clear all Keyframes?
-replaymod.gui.clearcallback.message=This Callback can be disabled in the Replay Settings.
-
-#Asset Manager Gui
-replaymod.gui.assets.title=Asset Manager
-replaymod.gui.assets.filechooser=Asset File
-replaymod.gui.assets.defaultname=New Asset
-replaymod.gui.assets.emptylist=No Assets available
-replaymod.gui.assets.namehint=Asset Name
-replaymod.gui.assets.changefile=Change Asset File
-replaymod.gui.assets.noselection=No Asset selected
-
-#Object Manager Gui
-replaymod.gui.objects.properties.anchor=Anchor Point
-replaymod.gui.objects.properties.position=Position
-replaymod.gui.objects.properties.scale=Scale
-replaymod.gui.objects.properties.orientation=Orientation
-replaymod.gui.objects.properties.opacity=Opacity
-replaymod.gui.objects.properties.name=Object Name
-replaymod.gui.objects.empty=No Objects added
-replaymod.gui.objects.defaultname=New Object
-replaymod.gui.objects=Custom Objects
-
-#Errors
-replaymod.error.unknownrestriction1=This replay cannot be played with your current version.
-replaymod.error.unknownrestriction2=It tried to enforce %s which is unknown.
-replaymod.error.negativetime1=Some of your time keyframes are out of order.
-replaymod.error.negativetime2=Going backwards in time is not supported.
-replaymod.error.negativetime3=The invalid parts are marked in red.
-
-#Replay Mod Incompatibility Warning
-replaymod.gui.modwarning.title=Incompatibility detected
-replaymod.gui.modwarning.message1=A possible incompatibility between your current
-replaymod.gui.modwarning.message2=Minecraft version and this Replay has been detected.
-replaymod.gui.modwarning.message3=Loading this Replay may result in errors or a crash.
-replaymod.gui.modwarning.name=Mod Name:
-replaymod.gui.modwarning.id=Mod Id:
-replaymod.gui.modwarning.missing=Missing Mods
-replaymod.gui.modwarning.version=Different Mod Versions
-replaymod.gui.modwarning.version.expected=Expected
-replaymod.gui.modwarning.version.found=Found
-
-#OpenEye
-replaymod.gui.offeropeneye1=Do you want to install OpenEye?
-replaymod.gui.offeropeneye2=OpenEye is a mod that collects anonymous information
-replaymod.gui.offeropeneye3=about the mods being run and any crashes that happen.
-replaymod.gui.offeropeneye4=
-replaymod.gui.offeropeneye5=OpenEye can be disabled at any time and is completely optional.
-replaymod.gui.offeropeneye6=More information is available at https://openeye.openmods.info/faq
-replaymod.gui.offeropeneye7=Clicking Yes will download OpenEye and place it in your mods folder.
-replaymod.gui.offeropeneye8=It will then be active the next time you start Minecraft.
\ No newline at end of file
diff --git a/src/test/java/com/replaymod/simplepathing/SPTimelineTest.java b/src/test/java/com/replaymod/simplepathing/SPTimelineTest.java
index de249b0e..36f0806f 100644
--- a/src/test/java/com/replaymod/simplepathing/SPTimelineTest.java
+++ b/src/test/java/com/replaymod/simplepathing/SPTimelineTest.java
@@ -473,6 +473,27 @@ public class SPTimelineTest {
impl.setInterpolatorToDefault(0);
}
+ @Test
+ public void testMoveKeyframeSimple() {
+ addPosition(0, 0);
+ addPosition(1, 1);
+ setInterpolator(0, new LinearInterpolator(), 1);
+ assertIsLinear(0);
+
+ impl.moveKeyframe(SPPath.POSITION, 0, 2);
+ assertValidInterpolators(SPPath.POSITION, 1);
+ assertIsLinear(0);
+ impl.moveKeyframe(SPPath.POSITION, 2, 0);
+ assertValidInterpolators(SPPath.POSITION, 1);
+ assertIsLinear(0);
+
+ addPosition(2, 1);
+ impl.moveKeyframe(SPPath.POSITION, 0, 3);
+ assertValidInterpolators(SPPath.POSITION, 1);
+ assertIsLinear(0);
+ assertIsLinear(1);
+ }
+
@Test
public void testMoveKeyframe() {
addPosition(1, 0);