From 4fc3a3166c48bde695703c21790aacca2127c1b6 Mon Sep 17 00:00:00 2001 From: johni0702 Date: Thu, 13 Apr 2017 18:06:21 +0200 Subject: [PATCH 01/24] Fix OpenEye being installed into working dir instead of mcDataDir --- src/main/java/com/replaymod/extras/OpenEyeExtra.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/replaymod/extras/OpenEyeExtra.java b/src/main/java/com/replaymod/extras/OpenEyeExtra.java index ad5c1004..72bdce4d 100644 --- a/src/main/java/com/replaymod/extras/OpenEyeExtra.java +++ b/src/main/java/com/replaymod/extras/OpenEyeExtra.java @@ -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(); From f58fa8f9c2568fe4e52783f419c5165b85d5d5f6 Mon Sep 17 00:00:00 2001 From: johni0702 Date: Thu, 13 Apr 2017 18:12:07 +0200 Subject: [PATCH 02/24] Fix Login GUI not respecting other GUIs opened on startup Previously the Login GUI would simply replace the currently active GUI. Now the currently active GUI is stored and then re-opened after login. --- src/main/java/com/replaymod/online/ReplayModOnline.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/replaymod/online/ReplayModOnline.java b/src/main/java/com/replaymod/online/ReplayModOnline.java index a833bbf6..84762330 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()); } } } From fe6ded0143235334da84ef0283e3a63e00e9f0b4 Mon Sep 17 00:00:00 2001 From: johni0702 Date: Thu, 13 Apr 2017 18:18:17 +0200 Subject: [PATCH 03/24] Fix movement of keyframe via GuiEditKeyframe not updating selected keyframe Selecting a keyframe, double clicking it to open the gui, changing the timestamp, saving and then clicking the Add/Remove Keyframe button used to crash the client. This is fixed by updating the selection whenever the time is changed. --- .../java/com/replaymod/simplepathing/gui/GuiEditKeyframe.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/com/replaymod/simplepathing/gui/GuiEditKeyframe.java b/src/main/java/com/replaymod/simplepathing/gui/GuiEditKeyframe.java index 7fa41072..0e74e9f2 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(); From fcbbbc9ea188d069d043ed5aff2e3de6be6864d4 Mon Sep 17 00:00:00 2001 From: johni0702 Date: Thu, 13 Apr 2017 18:38:58 +0200 Subject: [PATCH 04/24] Add integration test. Run with ./gradlew runIntegrationTest --- build.gradle | 31 ++ jGui | 2 +- .../java/IntegrationTest.java | 30 ++ .../java/com/replaymod/core/AbstractTask.java | 342 ++++++++++++++++++ .../com/replaymod/core/CompositeTask.java | 40 ++ .../core/ReplayModIntegrationTest.java | 69 ++++ .../java/com/replaymod/core/Task.java | 35 ++ .../java/com/replaymod/core/Utils.java | 31 ++ .../java/com/replaymod/core/Wait.java | 21 ++ .../core/regression/RegressionTest60.java | 59 +++ .../com/replaymod/extra/DownloadOpenEye.java | 25 ++ .../java/com/replaymod/online/SkipLogin.java | 14 + .../replaymod/recording/CreateSPWorld.java | 32 ++ .../com/replaymod/recording/ExitSPWorld.java | 36 ++ .../java/com/replaymod/replay/ExitReplay.java | 16 + .../java/com/replaymod/replay/LoadReplay.java | 32 ++ .../replaymod/replay/OpenReplayViewer.java | 15 + .../com/replaymod/replay/SpectatePlayer.java | 42 +++ .../replaymod/replay/overlay/OverlayGui.java | 33 ++ .../simplepathing/GuiPathingTasks.java | 35 ++ .../com/replaymod/extras/OpenEyeExtra.java | 4 +- .../replaymod/online/gui/GuiLoginPrompt.java | 2 +- .../simplepathing/ReplayModSimplePathing.java | 3 + 23 files changed, 945 insertions(+), 4 deletions(-) create mode 100644 src/integration-test/java/IntegrationTest.java create mode 100644 src/integration-test/java/com/replaymod/core/AbstractTask.java create mode 100644 src/integration-test/java/com/replaymod/core/CompositeTask.java create mode 100644 src/integration-test/java/com/replaymod/core/ReplayModIntegrationTest.java create mode 100644 src/integration-test/java/com/replaymod/core/Task.java create mode 100644 src/integration-test/java/com/replaymod/core/Utils.java create mode 100644 src/integration-test/java/com/replaymod/core/Wait.java create mode 100644 src/integration-test/java/com/replaymod/core/regression/RegressionTest60.java create mode 100644 src/integration-test/java/com/replaymod/extra/DownloadOpenEye.java create mode 100644 src/integration-test/java/com/replaymod/online/SkipLogin.java create mode 100644 src/integration-test/java/com/replaymod/recording/CreateSPWorld.java create mode 100644 src/integration-test/java/com/replaymod/recording/ExitSPWorld.java create mode 100644 src/integration-test/java/com/replaymod/replay/ExitReplay.java create mode 100644 src/integration-test/java/com/replaymod/replay/LoadReplay.java create mode 100644 src/integration-test/java/com/replaymod/replay/OpenReplayViewer.java create mode 100644 src/integration-test/java/com/replaymod/replay/SpectatePlayer.java create mode 100644 src/integration-test/java/com/replaymod/replay/overlay/OverlayGui.java create mode 100644 src/integration-test/java/com/replaymod/simplepathing/GuiPathingTasks.java diff --git a/build.gradle b/build.gradle index 57e98633..00d0511f 100755 --- a/build.gradle +++ b/build.gradle @@ -188,6 +188,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') { @@ -199,6 +206,30 @@ setupDecompWorkspace.dependsOn copySrg setupDevWorkspace.dependsOn copySrg project.tasks.idea.dependsOn copySrg +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 a77d2f3f..72e7a73b 160000 --- a/jGui +++ b/jGui @@ -1 +1 @@ -Subproject commit a77d2f3f50ba61ce68d2de66b798ec1e0a62723e +Subproject commit 72e7a73b65ab3751efb17b3f542484fd3947594c 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..9dd30b4d --- /dev/null +++ b/src/integration-test/java/com/replaymod/core/AbstractTask.java @@ -0,0 +1,342 @@ +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.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(); + + FMLCommonHandler.instance().bus().register(this); + MinecraftForge.EVENT_BUS.register(this); + addCallback(future, success -> { + FMLCommonHandler.instance().bus().unregister(this); + 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) { + FMLCommonHandler.instance().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); + } + } + } + } + FMLCommonHandler.instance().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) { + FMLCommonHandler.instance().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); + } + } + } + } + FMLCommonHandler.instance().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; + } + + FMLCommonHandler.instance().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)); + } + } + FMLCommonHandler.instance().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..3dca8f2f --- /dev/null +++ b/src/integration-test/java/com/replaymod/core/CompositeTask.java @@ -0,0 +1,40 @@ +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; + +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(); + Utils.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..bcf615ce --- /dev/null +++ b/src/integration-test/java/com/replaymod/core/ReplayModIntegrationTest.java @@ -0,0 +1,69 @@ +package com.replaymod.core; + +import com.replaymod.core.regression.RegressionTest60; +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.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 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..b732f25b --- /dev/null +++ b/src/integration-test/java/com/replaymod/core/Utils.java @@ -0,0 +1,31 @@ +package com.replaymod.core; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.function.Consumer; + +public class Utils { + 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 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/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..95f4cb16 --- /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.GuiSelectWorld; +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(GuiSelectWorld.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/extras/OpenEyeExtra.java b/src/main/java/com/replaymod/extras/OpenEyeExtra.java index 72bdce4d..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), @@ -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/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/simplepathing/ReplayModSimplePathing.java b/src/main/java/com/replaymod/simplepathing/ReplayModSimplePathing.java index 85dbd5d9..fec48686 100644 --- a/src/main/java/com/replaymod/simplepathing/ReplayModSimplePathing.java +++ b/src/main/java/com/replaymod/simplepathing/ReplayModSimplePathing.java @@ -22,6 +22,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; From 0c1dc658ad7c35734b79e698f7d222b8c87ddb1e Mon Sep 17 00:00:00 2001 From: johni0702 Date: Thu, 13 Apr 2017 20:39:45 +0200 Subject: [PATCH 05/24] Fix interpolator being lost when moving keyframe to the end (fixes #62) --- .../core/ReplayModIntegrationTest.java | 2 + .../core/regression/RegressionTest62.java | 28 ++++++++++++++ .../replaymod/simplepathing/SPTimeline.java | 37 ++++++++++++------- .../simplepathing/SPTimelineTest.java | 21 +++++++++++ 4 files changed, 75 insertions(+), 13 deletions(-) create mode 100644 src/integration-test/java/com/replaymod/core/regression/RegressionTest62.java diff --git a/src/integration-test/java/com/replaymod/core/ReplayModIntegrationTest.java b/src/integration-test/java/com/replaymod/core/ReplayModIntegrationTest.java index bcf615ce..6dd71cfe 100644 --- a/src/integration-test/java/com/replaymod/core/ReplayModIntegrationTest.java +++ b/src/integration-test/java/com/replaymod/core/ReplayModIntegrationTest.java @@ -1,6 +1,7 @@ package com.replaymod.core; import com.replaymod.core.regression.RegressionTest60; +import com.replaymod.core.regression.RegressionTest62; import com.replaymod.extra.DownloadOpenEye; import com.replaymod.online.SkipLogin; import com.replaymod.recording.CreateSPWorld; @@ -44,6 +45,7 @@ public class ReplayModIntegrationTest { new LoadReplay(), new RegressionTest60(), + new RegressionTest62(), // new AbstractTask() {}, // Uncomment to not exit on success (useful for writing more tests) new ExitReplay() 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/main/java/com/replaymod/simplepathing/SPTimeline.java b/src/main/java/com/replaymod/simplepathing/SPTimeline.java index b3cd30ad..e388ac13 100644 --- a/src/main/java/com/replaymod/simplepathing/SPTimeline.java +++ b/src/main/java/com/replaymod/simplepathing/SPTimeline.java @@ -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/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); From 03aada1f5f2a8b35168fc8b33c1929aa8c9e5e47 Mon Sep 17 00:00:00 2001 From: johni0702 Date: Thu, 13 Apr 2017 21:20:01 +0200 Subject: [PATCH 06/24] Update Mixin to 0.6.8 (fixes #9 GH) --- build.gradle | 2 +- src/main/java/com/replaymod/core/LoadingPlugin.java | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/build.gradle b/build.gradle index 00d0511f..7e3871ea 100755 --- a/build.gradle +++ b/build.gradle @@ -61,7 +61,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' diff --git a/src/main/java/com/replaymod/core/LoadingPlugin.java b/src/main/java/com/replaymod/core/LoadingPlugin.java index 6ed89aa4..2f0cc730 100755 --- a/src/main/java/com/replaymod/core/LoadingPlugin.java +++ b/src/main/java/com/replaymod/core/LoadingPlugin.java @@ -4,7 +4,7 @@ import net.minecraftforge.fml.relauncher.CoreModManager; import net.minecraftforge.fml.relauncher.IFMLLoadingPlugin; import org.apache.logging.log4j.LogManager; import org.spongepowered.asm.launch.MixinBootstrap; -import org.spongepowered.asm.mixin.MixinEnvironment; +import org.spongepowered.asm.mixin.Mixins; import java.io.File; import java.net.URISyntaxException; @@ -16,10 +16,10 @@ public class LoadingPlugin implements IFMLLoadingPlugin { public LoadingPlugin() { MixinBootstrap.init(); - MixinEnvironment.getDefaultEnvironment().addConfiguration("mixins.recording.replaymod.json"); - MixinEnvironment.getDefaultEnvironment().addConfiguration("mixins.render.replaymod.json"); - MixinEnvironment.getDefaultEnvironment().addConfiguration("mixins.replay.replaymod.json"); - MixinEnvironment.getDefaultEnvironment().addConfiguration("mixins.compat.shaders.replaymod.json"); + Mixins.addConfiguration("mixins.recording.replaymod.json"); + Mixins.addConfiguration("mixins.render.replaymod.json"); + Mixins.addConfiguration("mixins.replay.replaymod.json"); + Mixins.addConfiguration("mixins.compat.shaders.replaymod.json"); CodeSource codeSource = getClass().getProtectionDomain().getCodeSource(); if (codeSource != null) { From 40e1d850dac0adc7ea3e2130ded83e7b0ed2abe3 Mon Sep 17 00:00:00 2001 From: johni0702 Date: Fri, 14 Apr 2017 17:12:24 +0200 Subject: [PATCH 07/24] Fix crash when saving the last keyframe in the edit gui (fixes #61) --- .../core/ReplayModIntegrationTest.java | 2 ++ .../core/regression/RegressionTest61.java | 35 +++++++++++++++++++ .../simplepathing/gui/GuiEditKeyframe.java | 4 +++ 3 files changed, 41 insertions(+) create mode 100644 src/integration-test/java/com/replaymod/core/regression/RegressionTest61.java diff --git a/src/integration-test/java/com/replaymod/core/ReplayModIntegrationTest.java b/src/integration-test/java/com/replaymod/core/ReplayModIntegrationTest.java index 6dd71cfe..99fd80b0 100644 --- a/src/integration-test/java/com/replaymod/core/ReplayModIntegrationTest.java +++ b/src/integration-test/java/com/replaymod/core/ReplayModIntegrationTest.java @@ -1,6 +1,7 @@ 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; @@ -45,6 +46,7 @@ public class ReplayModIntegrationTest { new LoadReplay(), new RegressionTest60(), + new RegressionTest61(), new RegressionTest62(), // new AbstractTask() {}, // Uncomment to not exit on success (useful for writing more tests) 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/main/java/com/replaymod/simplepathing/gui/GuiEditKeyframe.java b/src/main/java/com/replaymod/simplepathing/gui/GuiEditKeyframe.java index 0e74e9f2..4a4747c0 100644 --- a/src/main/java/com/replaymod/simplepathing/gui/GuiEditKeyframe.java +++ b/src/main/java/com/replaymod/simplepathing/gui/GuiEditKeyframe.java @@ -255,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), From 3f0e3e725ee7ef33dcd9fae2764016ad79f374d8 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Fri, 21 Apr 2017 13:30:51 +0200 Subject: [PATCH 08/24] Fix NPE when receiving Replay|Restrict messages (fixes #16 GH) --- .../java/com/replaymod/recording/ReplayModRecording.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/replaymod/recording/ReplayModRecording.java b/src/main/java/com/replaymod/recording/ReplayModRecording.java index 7e38ebe8..9d1cd988 100644 --- a/src/main/java/com/replaymod/recording/ReplayModRecording.java +++ b/src/main/java/com/replaymod/recording/ReplayModRecording.java @@ -4,8 +4,10 @@ import com.replaymod.core.ReplayMod; import com.replaymod.core.utils.Restrictions; import com.replaymod.recording.handler.ConnectionEventHandler; import com.replaymod.recording.packet.PacketListener; -import net.minecraftforge.fml.common.FMLCommonHandler; +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandler; import net.minecraft.network.NetworkManager; +import net.minecraftforge.fml.common.FMLCommonHandler; import net.minecraftforge.fml.common.Mod; import net.minecraftforge.fml.common.event.FMLInitializationEvent; import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; @@ -54,7 +56,9 @@ public class ReplayModRecording { EventBus bus = FMLCommonHandler.instance().bus(); bus.register(connectionEventHandler = new ConnectionEventHandler(logger, core)); - NetworkRegistry.INSTANCE.newSimpleChannel(Restrictions.PLUGIN_CHANNEL); + @ChannelHandler.Sharable + class RestrictionsChannelHandler extends ChannelDuplexHandler {} + NetworkRegistry.INSTANCE.newChannel(Restrictions.PLUGIN_CHANNEL, new RestrictionsChannelHandler()); } public void initiateRecording(NetworkManager networkManager) { From dba085c700778fff2f78008378e8f130eac0daba Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Thu, 20 Apr 2017 17:40:58 +0200 Subject: [PATCH 09/24] Move translations into separate repo --- .gitmodules | 4 + build.gradle | 23 - src/main/resources/assets/replaymod/lang | 1 + .../assets/replaymod/lang/en_US.lang | 539 ------------------ 4 files changed, 5 insertions(+), 562 deletions(-) create mode 160000 src/main/resources/assets/replaymod/lang delete mode 100644 src/main/resources/assets/replaymod/lang/en_US.lang 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/build.gradle b/build.gradle index 7e3871ea..1ee97ba0 100755 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,5 @@ import groovy.json.JsonOutput -import java.util.zip.ZipInputStream - buildscript { repositories { mavenCentral() @@ -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 diff --git a/src/main/resources/assets/replaymod/lang b/src/main/resources/assets/replaymod/lang new file mode 160000 index 00000000..3df5521e --- /dev/null +++ b/src/main/resources/assets/replaymod/lang @@ -0,0 +1 @@ +Subproject commit 3df5521e08d26c00434f0ce6db01d7148e9576dc 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 From ee24866f29b8f3e41115f0764bf713b8bc71a965 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Thu, 20 Apr 2017 21:35:53 +0200 Subject: [PATCH 10/24] Fix upload replay button not being updated when name/tags change --- .../java/com/replaymod/online/gui/GuiUploadReplay.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) 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() { From 748a91e7b27669f3eadbf5a6d62c7275f6529422 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Thu, 20 Apr 2017 21:36:45 +0200 Subject: [PATCH 11/24] Fix missing tooltips on fav/like/dislike buttons in replay center --- .../com/replaymod/online/gui/GuiReplayCenter.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) 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")); } From 5677fc54cb53dfa2819a95c3a05da65e8e37f7c8 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Thu, 20 Apr 2017 22:34:03 +0200 Subject: [PATCH 12/24] Re-add warning to replay editor --- .../com/replaymod/editor/gui/GuiReplayEditor.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) 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); } }); } From 8a6ec5191bb18d04664acaabd03e7fffd558f466 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Thu, 20 Apr 2017 22:34:59 +0200 Subject: [PATCH 13/24] Add missing tooltips to player overview and various missing chat messages --- .../extras/playeroverview/PlayerOverviewGui.java | 12 +++++++++--- .../java/com/replaymod/replay/ReplayModReplay.java | 8 +++++++- .../com/replaymod/simplepathing/gui/GuiPathing.java | 6 ++++++ 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/replaymod/extras/playeroverview/PlayerOverviewGui.java b/src/main/java/com/replaymod/extras/playeroverview/PlayerOverviewGui.java index a979e841..9dcce774 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; @@ -36,6 +41,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 @@ -43,14 +49,14 @@ public class PlayerOverviewGui extends GuiScreen implements Closeable { getMinecraft().getSoundHandler().playSound(PositionedSoundRecord.create(BUTTON_SOUND, 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.create(BUTTON_SOUND, 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/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/simplepathing/gui/GuiPathing.java b/src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java index bde36719..8d61759c 100644 --- a/src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java +++ b/src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java @@ -300,9 +300,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); } From dfafbecf3526151dbd80e8bd3156d3e357b35c3e Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Fri, 21 Apr 2017 16:36:12 +0200 Subject: [PATCH 14/24] Load translations from github repo, only reload language not all resource packs --- .../replaymod/extras/LocalizationExtra.java | 114 +++++++++--------- .../com/replaymod/extras/ReplayModExtras.java | 6 +- 2 files changed, 62 insertions(+), 58 deletions(-) diff --git a/src/main/java/com/replaymod/extras/LocalizationExtra.java b/src/main/java/com/replaymod/extras/LocalizationExtra.java index 50e21aee..994c45e9 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.IMetadataSerializer; 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/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); } } } From 60879fbbedb6ab00ba9cc07b20e0f584b868917a Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Fri, 21 Apr 2017 17:24:43 +0200 Subject: [PATCH 15/24] Change local class to be an inner class because Srg2Source breaks with it --- .../java/com/replaymod/recording/ReplayModRecording.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/replaymod/recording/ReplayModRecording.java b/src/main/java/com/replaymod/recording/ReplayModRecording.java index 9d1cd988..b3b55768 100644 --- a/src/main/java/com/replaymod/recording/ReplayModRecording.java +++ b/src/main/java/com/replaymod/recording/ReplayModRecording.java @@ -56,11 +56,12 @@ public class ReplayModRecording { EventBus bus = FMLCommonHandler.instance().bus(); bus.register(connectionEventHandler = new ConnectionEventHandler(logger, core)); - @ChannelHandler.Sharable - class RestrictionsChannelHandler extends ChannelDuplexHandler {} NetworkRegistry.INSTANCE.newChannel(Restrictions.PLUGIN_CHANNEL, new RestrictionsChannelHandler()); } + @ChannelHandler.Sharable + private static class RestrictionsChannelHandler extends ChannelDuplexHandler {} + public void initiateRecording(NetworkManager networkManager) { connectionEventHandler.onConnectedToServerEvent(networkManager); } From a34bbbcd6852a2c3f5e42a0b9c4f355d38a1a8a0 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 21 May 2017 11:51:19 +0200 Subject: [PATCH 16/24] Fix NPE when spectating a player without a camera entity --- src/main/java/com/replaymod/replay/ReplayHandler.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/com/replaymod/replay/ReplayHandler.java b/src/main/java/com/replaymod/replay/ReplayHandler.java index 6d867cf4..8a9f4c82 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; From 0e0eaaa9a7429c05c264de13907b91aa256760d2 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 21 May 2017 12:17:51 +0200 Subject: [PATCH 17/24] Fix entity tracker being set even if it failed loading (fixes #50) --- src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java b/src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java index 8d61759c..6c41f625 100644 --- a/src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java +++ b/src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java @@ -499,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(() -> { From 391f304c5f3f1fae1a1470e0e875d1b65b22c8e8 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 21 May 2017 12:42:13 +0200 Subject: [PATCH 18/24] Show error popup if entity tracker fails to load --- .../java/com/replaymod/simplepathing/gui/GuiPathing.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java b/src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java index 6c41f625..c4c2b9c3 100644 --- a/src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java +++ b/src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java @@ -5,6 +5,7 @@ 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; @@ -564,7 +565,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; From f13297c5a5a2000bb932d4166c242c74abddf54b Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 21 May 2017 16:29:59 +0200 Subject: [PATCH 19/24] Fix default interpolator handling in keyframe gui and after loading (fixes #64) --- .../com/replaymod/simplepathing/ReplayModSimplePathing.java | 4 ++-- src/main/java/com/replaymod/simplepathing/SPTimeline.java | 2 +- .../java/com/replaymod/simplepathing/gui/GuiEditKeyframe.java | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/replaymod/simplepathing/ReplayModSimplePathing.java b/src/main/java/com/replaymod/simplepathing/ReplayModSimplePathing.java index fec48686..b3b25847 100644 --- a/src/main/java/com/replaymod/simplepathing/ReplayModSimplePathing.java +++ b/src/main/java/com/replaymod/simplepathing/ReplayModSimplePathing.java @@ -46,7 +46,7 @@ public class ReplayModSimplePathing { @SubscribeEvent public void postReplayOpen(ReplayOpenEvent.Post event) { - currentTimeline = new SPTimeline(); + clearCurrentTimeline(); guiPathing = new GuiPathing(core, this, event.getReplayHandler()); } @@ -85,11 +85,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 e388ac13..71cde911 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()); diff --git a/src/main/java/com/replaymod/simplepathing/gui/GuiEditKeyframe.java b/src/main/java/com/replaymod/simplepathing/gui/GuiEditKeyframe.java index 4a4747c0..1b9e8bc0 100644 --- a/src/main/java/com/replaymod/simplepathing/gui/GuiEditKeyframe.java +++ b/src/main/java/com/replaymod/simplepathing/gui/GuiEditKeyframe.java @@ -310,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 From c054fe83f6d4a0b5573e9e10d6b44fd93d35fec3 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 21 May 2017 16:54:37 +0200 Subject: [PATCH 20/24] Register keys for simplepathing only once (fixes #63) --- .../simplepathing/ReplayModSimplePathing.java | 14 ++ .../simplepathing/gui/GuiPathing.java | 145 +++++++++--------- 2 files changed, 86 insertions(+), 73 deletions(-) diff --git a/src/main/java/com/replaymod/simplepathing/ReplayModSimplePathing.java b/src/main/java/com/replaymod/simplepathing/ReplayModSimplePathing.java index b3b25847..9f445ca9 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@", @@ -42,6 +43,19 @@ 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 diff --git a/src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java b/src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java index c4c2b9c3..77854930 100644 --- a/src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java +++ b/src/main/java/com/replaymod/simplepathing/gui/GuiPathing.java @@ -1,5 +1,6 @@ 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; @@ -256,6 +257,7 @@ public class GuiPathing { } }; + private final ReplayMod core; private final ReplayModSimplePathing mod; private final ReplayHandler replayHandler; private final RealtimeTimelinePlayer player; @@ -265,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); @@ -400,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(() -> { From 18c5bcd469fc03474c82abf0a438f5871787e702 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 21 May 2017 18:59:29 +0200 Subject: [PATCH 21/24] Be more cooperative with other mods on the ingame menu (fixes #42) --- .../replaymod/replay/handler/GuiHandler.java | 36 +++++++++++++++---- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/replaymod/replay/handler/GuiHandler.java b/src/main/java/com/replaymod/replay/handler/GuiHandler.java index 23e9b657..f05d2971 100644 --- a/src/main/java/com/replaymod/replay/handler/GuiHandler.java +++ b/src/main/java/com/replaymod/replay/handler/GuiHandler.java @@ -18,7 +18,6 @@ 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; @@ -49,6 +48,7 @@ public class GuiHandler { // Pause replay when menu is opened mod.getReplayHandler().getReplaySender().setReplaySpeed(0); + GuiButton achievements = null, stats = null, openToLan = null; @SuppressWarnings("unchecked") List buttonList = event.buttonList; for(GuiButton b : new ArrayList<>(buttonList)) { @@ -60,15 +60,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: - buttonList.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; + } } } From 1a55983986f8f485c87ef0532930c1287288e640 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 21 May 2017 19:30:37 +0200 Subject: [PATCH 22/24] Move addCallback method from integration test Utils to main Utils --- .../java/com/replaymod/core/AbstractTask.java | 2 +- .../com/replaymod/core/CompositeTask.java | 3 ++- .../core/ReplayModIntegrationTest.java | 2 +- .../java/com/replaymod/core/Utils.java | 22 ------------------- .../java/com/replaymod/core/utils/Utils.java | 17 ++++++++++++++ 5 files changed, 21 insertions(+), 25 deletions(-) diff --git a/src/integration-test/java/com/replaymod/core/AbstractTask.java b/src/integration-test/java/com/replaymod/core/AbstractTask.java index 9dd30b4d..e21d7b52 100644 --- a/src/integration-test/java/com/replaymod/core/AbstractTask.java +++ b/src/integration-test/java/com/replaymod/core/AbstractTask.java @@ -30,7 +30,7 @@ import java.util.NoSuchElementException; import java.util.concurrent.TimeoutException; import static com.replaymod.core.ReplayModIntegrationTest.LOGGER; -import static com.replaymod.core.Utils.addCallback; +import static com.replaymod.core.utils.Utils.addCallback; public abstract class AbstractTask implements Task { public static Task create(Consumer init) { diff --git a/src/integration-test/java/com/replaymod/core/CompositeTask.java b/src/integration-test/java/com/replaymod/core/CompositeTask.java index 3dca8f2f..deeb2529 100644 --- a/src/integration-test/java/com/replaymod/core/CompositeTask.java +++ b/src/integration-test/java/com/replaymod/core/CompositeTask.java @@ -4,6 +4,7 @@ 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; @@ -28,7 +29,7 @@ public class CompositeTask implements Task { Task task = children[childIndex]; LOGGER.info("Running task {}", task); ListenableFuture childFuture = task.execute(); - Utils.addCallback(childFuture, done -> executeChild(childIndex + 1), err -> future.setException(err)); + addCallback(childFuture, done -> executeChild(childIndex + 1), err -> future.setException(err)); } catch (Throwable t) { future.setException(t); } diff --git a/src/integration-test/java/com/replaymod/core/ReplayModIntegrationTest.java b/src/integration-test/java/com/replaymod/core/ReplayModIntegrationTest.java index 99fd80b0..3fc0d00f 100644 --- a/src/integration-test/java/com/replaymod/core/ReplayModIntegrationTest.java +++ b/src/integration-test/java/com/replaymod/core/ReplayModIntegrationTest.java @@ -18,7 +18,7 @@ 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.addCallback; +import static com.replaymod.core.utils.Utils.addCallback; /** * Helper mod that initiates the integration tests. diff --git a/src/integration-test/java/com/replaymod/core/Utils.java b/src/integration-test/java/com/replaymod/core/Utils.java index b732f25b..61d41497 100644 --- a/src/integration-test/java/com/replaymod/core/Utils.java +++ b/src/integration-test/java/com/replaymod/core/Utils.java @@ -1,28 +1,6 @@ package com.replaymod.core; -import com.google.common.util.concurrent.FutureCallback; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.util.function.Consumer; - public class Utils { - 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 void times(int x, Runnable runnable) { for (int i = 0; i < x; i++) { runnable.run(); diff --git a/src/main/java/com/replaymod/core/utils/Utils.java b/src/main/java/com/replaymod/core/utils/Utils.java index 8b27fff4..3252d06c 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(); From d71358bcbadac66b771239c7f089b67954371ee5 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 21 May 2017 19:31:28 +0200 Subject: [PATCH 23/24] Add confirmation dialog before overwriting path presets (fixes #65) --- .../pathing/gui/GuiKeyframeRepository.java | 20 +++++++++++++++---- src/main/resources/assets/replaymod/lang | 2 +- 2 files changed, 17 insertions(+), 5 deletions(-) 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/resources/assets/replaymod/lang b/src/main/resources/assets/replaymod/lang index 3df5521e..5db4b39d 160000 --- a/src/main/resources/assets/replaymod/lang +++ b/src/main/resources/assets/replaymod/lang @@ -1 +1 @@ -Subproject commit 3df5521e08d26c00434f0ce6db01d7148e9576dc +Subproject commit 5db4b39d23844e5f5e8a10f128035c81237d6541 From 91573d948790feb4702b3455a888773b79f5f8e5 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Wed, 31 May 2017 09:15:57 +0200 Subject: [PATCH 24/24] Update jGui, ReplayStudio and Translations jGui: 9e84e72 Fix inversion of empty selection ReplayStudio: ff12b1d Remove invalid filter from services files c6a9325 Proper value wrapping in CatmullRomSplineInterpolator (fixes #8 GH) --- ReplayStudio | 2 +- jGui | 2 +- src/main/resources/assets/replaymod/lang | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ReplayStudio b/ReplayStudio index c6154766..ff12b1db 160000 --- a/ReplayStudio +++ b/ReplayStudio @@ -1 +1 @@ -Subproject commit c615476693961345d04320e69f6558527ab73a59 +Subproject commit ff12b1db0b7e96a73e1a15a6b344d65b5b836050 diff --git a/jGui b/jGui index 72e7a73b..9e84e724 160000 --- a/jGui +++ b/jGui @@ -1 +1 @@ -Subproject commit 72e7a73b65ab3751efb17b3f542484fd3947594c +Subproject commit 9e84e724aaf7de21a06fd534faccdfdc6740fc28 diff --git a/src/main/resources/assets/replaymod/lang b/src/main/resources/assets/replaymod/lang index 5db4b39d..da6f6f8d 160000 --- a/src/main/resources/assets/replaymod/lang +++ b/src/main/resources/assets/replaymod/lang @@ -1 +1 @@ -Subproject commit 5db4b39d23844e5f5e8a10f128035c81237d6541 +Subproject commit da6f6f8d123cbda3d04e027948e29161ce302bdc