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 44f7345b..c283a442 160000 --- a/ReplayStudio +++ b/ReplayStudio @@ -1 +1 @@ -Subproject commit 44f7345b6be453ba68e5258095fd40e726d8e95e +Subproject commit c283a442577b62d850e7dfd86e9b3f3836f2e954 diff --git a/build.gradle b/build.gradle index ed22a545..13fc5685 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') { @@ -251,6 +235,30 @@ tasks.deobfMcMCP.doLast { 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 26af9162..e2bd9062 160000 --- a/jGui +++ b/jGui @@ -1 +1 @@ -Subproject commit 26af916257cbfe7da73a1f1b7af49219cc39885b +Subproject commit e2bd90628e29809609f7dad0acb1ab3adab2b7c8 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..e024f347 --- /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.thePlayer) { + 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 e2b53b55..118f262f 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);