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;