From 9af40eabc19dff7237100d6ababb49ad734ee346 Mon Sep 17 00:00:00 2001 From: joe Date: Tue, 10 Dec 2024 14:55:49 +0000 Subject: [PATCH 01/15] Move client auto tests to new module fabric-client-gametest-api-v1 --- build.gradle | 16 ++- .../src/testmod/resources/fabric.mod.json | 11 +- fabric-client-gametest-api-v1/build.gradle | 6 + .../ClientGameTestMixinConfigPlugin.java | 59 ++++++++++ .../client/FabricClientTestHelper.java | 8 +- .../gametest}/client/TestDedicatedServer.java | 2 +- .../impl/gametest}/client/ThreadingImpl.java | 2 +- .../client}/CyclingButtonWidgetAccessor.java | 2 +- .../client}/MinecraftClientMixin.java | 103 ++++++++---------- .../MinecraftDedicatedServerMixin.java | 4 +- .../client}/MinecraftServerMixin.java | 65 +++++------ .../gametest/client}/ScreenAccessor.java | 2 +- .../gametest/client}/TitleScreenAccessor.java | 2 +- .../fabric-client-gametest-api-v1/icon.png | Bin 0 -> 1579 bytes .../fabric-client-gametest-api-v1.mixins.json | 17 +-- .../src/client/resources/fabric.mod.json | 29 +++++ .../client/ClientGameTestTestMod.java | 40 +++---- .../testmodClient/resources/fabric.mod.json | 13 +++ gradle.properties | 1 + settings.gradle | 1 + 20 files changed, 235 insertions(+), 148 deletions(-) create mode 100644 fabric-client-gametest-api-v1/build.gradle create mode 100644 fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/gametest/client/ClientGameTestMixinConfigPlugin.java rename {fabric-api-base/src/testmodClient/java/net/fabricmc/fabric/test/base => fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/gametest}/client/FabricClientTestHelper.java (96%) rename {fabric-api-base/src/testmodClient/java/net/fabricmc/fabric/test/base => fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/gametest}/client/TestDedicatedServer.java (98%) rename {fabric-api-base/src/testmodClient/java/net/fabricmc/fabric/test/base => fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/gametest}/client/ThreadingImpl.java (99%) rename {fabric-api-base/src/testmodClient/java/net/fabricmc/fabric/test/base/client/mixin => fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/gametest/client}/CyclingButtonWidgetAccessor.java (94%) rename {fabric-api-base/src/testmodClient/java/net/fabricmc/fabric/test/base/client/mixin => fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/gametest/client}/MinecraftClientMixin.java (60%) rename {fabric-api-base/src/testmodClient/java/net/fabricmc/fabric/test/base/client/mixin => fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/gametest/client}/MinecraftDedicatedServerMixin.java (93%) rename {fabric-api-base/src/testmodClient/java/net/fabricmc/fabric/test/base/client/mixin => fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/gametest/client}/MinecraftServerMixin.java (54%) rename {fabric-api-base/src/testmodClient/java/net/fabricmc/fabric/test/base/client/mixin => fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/gametest/client}/ScreenAccessor.java (94%) rename {fabric-api-base/src/testmodClient/java/net/fabricmc/fabric/test/base/client/mixin => fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/gametest/client}/TitleScreenAccessor.java (94%) create mode 100644 fabric-client-gametest-api-v1/src/client/resources/assets/fabric-client-gametest-api-v1/icon.png rename fabric-api-base/src/testmodClient/resources/fabric-api-base-testmod.client.mixins.json => fabric-client-gametest-api-v1/src/client/resources/fabric-client-gametest-api-v1.mixins.json (68%) create mode 100644 fabric-client-gametest-api-v1/src/client/resources/fabric.mod.json rename fabric-api-base/src/testmodClient/java/net/fabricmc/fabric/test/base/client/FabricApiAutoTestClient.java => fabric-client-gametest-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/gametest/client/ClientGameTestTestMod.java (72%) create mode 100644 fabric-client-gametest-api-v1/src/testmodClient/resources/fabric.mod.json diff --git a/build.gradle b/build.gradle index 7e71f6b344..b9b44f79c8 100644 --- a/build.gradle +++ b/build.gradle @@ -420,7 +420,7 @@ loom { autoTestClient { inherit testmodClient name "Auto Test Client" - vmArg "-Dfabric.autoTest" + vmArg "-Dfabric.client.gametest" vmArg "-Dfabric-tag-conventions-v2.missingTagTranslationWarning=fail" vmArg "-Dfabric-tag-conventions-v1.legacyTagWarning=fail" } @@ -498,6 +498,9 @@ import net.fabricmc.loom.util.Platform // This is very far beyond loom's API if you copy this, you're on your own. tasks.register('runProductionAutoTestClient', JavaExec) { dependsOn remapJar, remapTestmodJar, downloadAssets + afterEvaluate { + dependsOn project(':fabric-client-gametest-api-v1').remapJar + } classpath.from configurations.productionRuntime mainClass = "net.fabricmc.loader.impl.launch.knot.KnotClient" workingDir = file("run") @@ -518,9 +521,11 @@ tasks.register('runProductionAutoTestClient', JavaExec) { ) } + afterEvaluate { + jvmArgs "-Dfabric.addMods=${remapJar.archiveFile.get().asFile.absolutePath}${File.pathSeparator}${remapTestmodJar.archiveFile.get().asFile.absolutePath}${File.pathSeparator}${project(':fabric-client-gametest-api-v1').remapJar.archiveFile.get().asFile.absolutePath}" + } jvmArgs( - "-Dfabric.addMods=${remapJar.archiveFile.get().asFile.absolutePath}${File.pathSeparator}${remapTestmodJar.archiveFile.get().asFile.absolutePath}", - "-Dfabric.autoTest", + "-Dfabric.gametest.client", "-Dfabric-tag-conventions-v2.missingTagTranslationWarning=fail", "-Dfabric-tag-conventions-v1.legacyTagWarning=fail" ) @@ -726,7 +731,10 @@ subprojects.each { } // These modules are not included in the fat jar, maven will resolve them via the pom. -def devOnlyModules = ["fabric-gametest-api-v1",] +def devOnlyModules = [ + "fabric-client-gametest-api-v1", + "fabric-gametest-api-v1", +] dependencies { afterEvaluate { diff --git a/fabric-api-base/src/testmod/resources/fabric.mod.json b/fabric-api-base/src/testmod/resources/fabric.mod.json index 05db3dc8f3..fc1240a71a 100644 --- a/fabric-api-base/src/testmod/resources/fabric.mod.json +++ b/fabric-api-base/src/testmod/resources/fabric.mod.json @@ -9,20 +9,11 @@ "main": [ "net.fabricmc.fabric.test.base.FabricApiBaseTestInit" ], - "client": [ - "net.fabricmc.fabric.test.base.client.FabricApiAutoTestClient" - ], "server": [ "net.fabricmc.fabric.test.base.FabricApiAutoTestServer" ], "fabric-gametest" : [ "net.fabricmc.fabric.test.base.FabricApiBaseGameTest" ] - }, - "mixins": [ - { - "config": "fabric-api-base-testmod.client.mixins.json", - "environment": "client" - } - ] + } } diff --git a/fabric-client-gametest-api-v1/build.gradle b/fabric-client-gametest-api-v1/build.gradle new file mode 100644 index 0000000000..f2db1b51aa --- /dev/null +++ b/fabric-client-gametest-api-v1/build.gradle @@ -0,0 +1,6 @@ +version = getSubprojectVersion(project) + +moduleDependencies(project, [ + 'fabric-api-base', + 'fabric-resource-loader-v0' +]) diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/gametest/client/ClientGameTestMixinConfigPlugin.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/gametest/client/ClientGameTestMixinConfigPlugin.java new file mode 100644 index 0000000000..79c0c68778 --- /dev/null +++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/gametest/client/ClientGameTestMixinConfigPlugin.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.impl.gametest.client; + +import java.util.List; +import java.util.Set; + +import org.objectweb.asm.tree.ClassNode; +import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin; +import org.spongepowered.asm.mixin.extensibility.IMixinInfo; + +public class ClientGameTestMixinConfigPlugin implements IMixinConfigPlugin { + private static final boolean ENABLED = System.getProperty("fabric.client.gametest") != null; + + @Override + public void onLoad(String mixinPackage) { + } + + @Override + public String getRefMapperConfig() { + return null; + } + + @Override + public boolean shouldApplyMixin(String targetClassName, String mixinClassName) { + return ENABLED; + } + + @Override + public void acceptTargets(Set myTargets, Set otherTargets) { + } + + @Override + public List getMixins() { + return null; + } + + @Override + public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { + } + + @Override + public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { + } +} diff --git a/fabric-api-base/src/testmodClient/java/net/fabricmc/fabric/test/base/client/FabricClientTestHelper.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/gametest/client/FabricClientTestHelper.java similarity index 96% rename from fabric-api-base/src/testmodClient/java/net/fabricmc/fabric/test/base/client/FabricClientTestHelper.java rename to fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/gametest/client/FabricClientTestHelper.java index 79e194fb77..299663fffd 100644 --- a/fabric-api-base/src/testmodClient/java/net/fabricmc/fabric/test/base/client/FabricClientTestHelper.java +++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/gametest/client/FabricClientTestHelper.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.fabricmc.fabric.test.base.client; +package net.fabricmc.fabric.impl.gametest.client; import java.util.Objects; import java.util.function.Function; @@ -45,9 +45,9 @@ import net.minecraft.client.util.ScreenshotRecorder; import net.minecraft.text.Text; -import net.fabricmc.fabric.test.base.client.mixin.CyclingButtonWidgetAccessor; -import net.fabricmc.fabric.test.base.client.mixin.ScreenAccessor; -import net.fabricmc.fabric.test.base.client.mixin.TitleScreenAccessor; +import net.fabricmc.fabric.mixin.gametest.client.CyclingButtonWidgetAccessor; +import net.fabricmc.fabric.mixin.gametest.client.ScreenAccessor; +import net.fabricmc.fabric.mixin.gametest.client.TitleScreenAccessor; import net.fabricmc.loader.api.FabricLoader; public final class FabricClientTestHelper { diff --git a/fabric-api-base/src/testmodClient/java/net/fabricmc/fabric/test/base/client/TestDedicatedServer.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/gametest/client/TestDedicatedServer.java similarity index 98% rename from fabric-api-base/src/testmodClient/java/net/fabricmc/fabric/test/base/client/TestDedicatedServer.java rename to fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/gametest/client/TestDedicatedServer.java index 58796eb6e2..7cc62379c6 100644 --- a/fabric-api-base/src/testmodClient/java/net/fabricmc/fabric/test/base/client/TestDedicatedServer.java +++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/gametest/client/TestDedicatedServer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.fabricmc.fabric.test.base.client; +package net.fabricmc.fabric.impl.gametest.client; import java.io.Closeable; import java.io.IOException; diff --git a/fabric-api-base/src/testmodClient/java/net/fabricmc/fabric/test/base/client/ThreadingImpl.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/gametest/client/ThreadingImpl.java similarity index 99% rename from fabric-api-base/src/testmodClient/java/net/fabricmc/fabric/test/base/client/ThreadingImpl.java rename to fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/gametest/client/ThreadingImpl.java index ee13bcf40b..87da7f9264 100644 --- a/fabric-api-base/src/testmodClient/java/net/fabricmc/fabric/test/base/client/ThreadingImpl.java +++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/gametest/client/ThreadingImpl.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.fabricmc.fabric.test.base.client; +package net.fabricmc.fabric.impl.gametest.client; import java.util.concurrent.Phaser; import java.util.concurrent.Semaphore; diff --git a/fabric-api-base/src/testmodClient/java/net/fabricmc/fabric/test/base/client/mixin/CyclingButtonWidgetAccessor.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/gametest/client/CyclingButtonWidgetAccessor.java similarity index 94% rename from fabric-api-base/src/testmodClient/java/net/fabricmc/fabric/test/base/client/mixin/CyclingButtonWidgetAccessor.java rename to fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/gametest/client/CyclingButtonWidgetAccessor.java index cca8b118b2..2b8fd4a9d6 100644 --- a/fabric-api-base/src/testmodClient/java/net/fabricmc/fabric/test/base/client/mixin/CyclingButtonWidgetAccessor.java +++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/gametest/client/CyclingButtonWidgetAccessor.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.fabricmc.fabric.test.base.client.mixin; +package net.fabricmc.fabric.mixin.gametest.client; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.gen.Accessor; diff --git a/fabric-api-base/src/testmodClient/java/net/fabricmc/fabric/test/base/client/mixin/MinecraftClientMixin.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/gametest/client/MinecraftClientMixin.java similarity index 60% rename from fabric-api-base/src/testmodClient/java/net/fabricmc/fabric/test/base/client/mixin/MinecraftClientMixin.java rename to fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/gametest/client/MinecraftClientMixin.java index 137d4defa5..d5ddd343cb 100644 --- a/fabric-api-base/src/testmodClient/java/net/fabricmc/fabric/test/base/client/mixin/MinecraftClientMixin.java +++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/gametest/client/MinecraftClientMixin.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.fabricmc.fabric.test.base.client.mixin; +package net.fabricmc.fabric.mixin.gametest.client; import com.google.common.base.Preconditions; import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; @@ -32,8 +32,7 @@ import net.minecraft.server.SaveLoader; import net.minecraft.world.level.storage.LevelStorage; -import net.fabricmc.fabric.test.base.client.FabricApiAutoTestClient; -import net.fabricmc.fabric.test.base.client.ThreadingImpl; +import net.fabricmc.fabric.impl.gametest.client.ThreadingImpl; @Mixin(MinecraftClient.class) public class MinecraftClientMixin { @@ -42,71 +41,63 @@ public class MinecraftClientMixin { @WrapMethod(method = "run") private void onRun(Operation original) { - if (FabricApiAutoTestClient.IS_AUTO_TEST) { - if (ThreadingImpl.isClientRunning) { - throw new IllegalStateException("Client is already running"); - } - - ThreadingImpl.isClientRunning = true; - ThreadingImpl.PHASER.register(); + if (ThreadingImpl.isClientRunning) { + throw new IllegalStateException("Client is already running"); } + ThreadingImpl.isClientRunning = true; + ThreadingImpl.PHASER.register(); + try { original.call(); } finally { - if (FabricApiAutoTestClient.IS_AUTO_TEST) { - ThreadingImpl.clientCanAcceptTasks = false; - ThreadingImpl.PHASER.arriveAndDeregister(); - ThreadingImpl.isClientRunning = false; - } + ThreadingImpl.clientCanAcceptTasks = false; + ThreadingImpl.PHASER.arriveAndDeregister(); + ThreadingImpl.isClientRunning = false; } } @Inject(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/MinecraftClient;runTasks()V")) private void preRunTasks(CallbackInfo ci) { - if (FabricApiAutoTestClient.IS_AUTO_TEST) { - ThreadingImpl.enterPhase(ThreadingImpl.PHASE_SERVER_TASKS); - // server tasks happen here - ThreadingImpl.enterPhase(ThreadingImpl.PHASE_CLIENT_TASKS); - } + ThreadingImpl.enterPhase(ThreadingImpl.PHASE_SERVER_TASKS); + // server tasks happen here + ThreadingImpl.enterPhase(ThreadingImpl.PHASE_CLIENT_TASKS); } @Inject(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/MinecraftClient;runTasks()V", shift = At.Shift.AFTER)) private void postRunTasks(CallbackInfo ci) { - if (FabricApiAutoTestClient.IS_AUTO_TEST) { - ThreadingImpl.clientCanAcceptTasks = true; - ThreadingImpl.enterPhase(ThreadingImpl.PHASE_TEST); - - if (ThreadingImpl.testThread != null) { - while (true) { - try { - ThreadingImpl.CLIENT_SEMAPHORE.acquire(); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - - if (ThreadingImpl.taskToRun != null) { - ThreadingImpl.taskToRun.run(); - } else { - break; - } + ThreadingImpl.clientCanAcceptTasks = true; + ThreadingImpl.enterPhase(ThreadingImpl.PHASE_TEST); + + if (ThreadingImpl.testThread != null) { + while (true) { + try { + ThreadingImpl.CLIENT_SEMAPHORE.acquire(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + if (ThreadingImpl.taskToRun != null) { + ThreadingImpl.taskToRun.run(); + } else { + break; } } + } - ThreadingImpl.enterPhase(ThreadingImpl.PHASE_TICK); + ThreadingImpl.enterPhase(ThreadingImpl.PHASE_TICK); - Runnable deferredTask = this.deferredTask; - this.deferredTask = null; + Runnable deferredTask = this.deferredTask; + this.deferredTask = null; - if (deferredTask != null) { - deferredTask.run(); - } + if (deferredTask != null) { + deferredTask.run(); } } @Inject(method = "startIntegratedServer", at = @At("HEAD"), cancellable = true) private void deferStartIntegratedServer(LevelStorage.Session session, ResourcePackManager dataPackManager, SaveLoader saveLoader, boolean newWorld, CallbackInfo ci) { - if (FabricApiAutoTestClient.IS_AUTO_TEST && ThreadingImpl.taskToRun != null) { + if (ThreadingImpl.taskToRun != null) { // don't start the integrated server (which busywaits) inside a task deferredTask = () -> MinecraftClient.getInstance().startIntegratedServer(session, dataPackManager, saveLoader, newWorld); ci.cancel(); @@ -115,16 +106,14 @@ private void deferStartIntegratedServer(LevelStorage.Session session, ResourcePa @Inject(method = "startIntegratedServer", at = @At(value = "INVOKE", target = "Ljava/lang/Thread;sleep(J)V", remap = false)) private void onStartIntegratedServerBusyWait(CallbackInfo ci) { - if (FabricApiAutoTestClient.IS_AUTO_TEST) { - // give the server a chance to tick too - preRunTasks(ci); - postRunTasks(ci); - } + // give the server a chance to tick too + preRunTasks(ci); + postRunTasks(ci); } @Inject(method = "disconnect(Lnet/minecraft/client/gui/screen/Screen;Z)V", at = @At("HEAD"), cancellable = true) private void deferDisconnect(Screen disconnectionScreen, boolean transferring, CallbackInfo ci) { - if (FabricApiAutoTestClient.IS_AUTO_TEST && MinecraftClient.getInstance().getServer() != null && ThreadingImpl.taskToRun != null) { + if (MinecraftClient.getInstance().getServer() != null && ThreadingImpl.taskToRun != null) { // don't disconnect (which busywaits) inside a task deferredTask = () -> MinecraftClient.getInstance().disconnect(disconnectionScreen, transferring); ci.cancel(); @@ -133,18 +122,14 @@ private void deferDisconnect(Screen disconnectionScreen, boolean transferring, C @Inject(method = "disconnect(Lnet/minecraft/client/gui/screen/Screen;Z)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/MinecraftClient;render(Z)V", shift = At.Shift.AFTER)) private void onDisconnectBusyWait(CallbackInfo ci) { - if (FabricApiAutoTestClient.IS_AUTO_TEST) { - // give the server a chance to tick too - preRunTasks(ci); - postRunTasks(ci); - } + // give the server a chance to tick too + preRunTasks(ci); + postRunTasks(ci); } @Inject(method = "getInstance", at = @At("HEAD")) private static void checkThreadOnGetInstance(CallbackInfoReturnable cir) { - if (FabricApiAutoTestClient.IS_AUTO_TEST) { - // TODO: add suggestion of runOnClient etc when API methods are added - Preconditions.checkState(Thread.currentThread() != ThreadingImpl.testThread, "MinecraftClient.getInstance() cannot be called from the test thread"); - } + // TODO: add suggestion of runOnClient etc when API methods are added + Preconditions.checkState(Thread.currentThread() != ThreadingImpl.testThread, "MinecraftClient.getInstance() cannot be called from the test thread"); } } diff --git a/fabric-api-base/src/testmodClient/java/net/fabricmc/fabric/test/base/client/mixin/MinecraftDedicatedServerMixin.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/gametest/client/MinecraftDedicatedServerMixin.java similarity index 93% rename from fabric-api-base/src/testmodClient/java/net/fabricmc/fabric/test/base/client/mixin/MinecraftDedicatedServerMixin.java rename to fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/gametest/client/MinecraftDedicatedServerMixin.java index e53a5256ed..c5e7ece359 100644 --- a/fabric-api-base/src/testmodClient/java/net/fabricmc/fabric/test/base/client/mixin/MinecraftDedicatedServerMixin.java +++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/gametest/client/MinecraftDedicatedServerMixin.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.fabricmc.fabric.test.base.client.mixin; +package net.fabricmc.fabric.mixin.gametest.client; import com.llamalad7.mixinextras.injector.wrapoperation.Operation; import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; @@ -25,7 +25,7 @@ import net.minecraft.server.dedicated.MinecraftDedicatedServer; -import net.fabricmc.fabric.test.base.client.TestDedicatedServer; +import net.fabricmc.fabric.impl.gametest.client.TestDedicatedServer; @Mixin(MinecraftDedicatedServer.class) public abstract class MinecraftDedicatedServerMixin { diff --git a/fabric-api-base/src/testmodClient/java/net/fabricmc/fabric/test/base/client/mixin/MinecraftServerMixin.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/gametest/client/MinecraftServerMixin.java similarity index 54% rename from fabric-api-base/src/testmodClient/java/net/fabricmc/fabric/test/base/client/mixin/MinecraftServerMixin.java rename to fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/gametest/client/MinecraftServerMixin.java index 9b044be19b..702cd87efe 100644 --- a/fabric-api-base/src/testmodClient/java/net/fabricmc/fabric/test/base/client/mixin/MinecraftServerMixin.java +++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/gametest/client/MinecraftServerMixin.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.fabricmc.fabric.test.base.client.mixin; +package net.fabricmc.fabric.mixin.gametest.client; import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; import com.llamalad7.mixinextras.injector.wrapoperation.Operation; @@ -25,66 +25,57 @@ import net.minecraft.server.MinecraftServer; -import net.fabricmc.fabric.test.base.client.FabricApiAutoTestClient; -import net.fabricmc.fabric.test.base.client.ThreadingImpl; +import net.fabricmc.fabric.impl.gametest.client.ThreadingImpl; @Mixin(MinecraftServer.class) public class MinecraftServerMixin { @WrapMethod(method = "runServer") private void onRunServer(Operation original) { - if (FabricApiAutoTestClient.IS_AUTO_TEST) { - if (ThreadingImpl.isServerRunning) { - throw new IllegalStateException("Server is already running"); - } - - ThreadingImpl.isServerRunning = true; - ThreadingImpl.PHASER.register(); + if (ThreadingImpl.isServerRunning) { + throw new IllegalStateException("Server is already running"); } + ThreadingImpl.isServerRunning = true; + ThreadingImpl.PHASER.register(); + try { original.call(); } finally { - if (FabricApiAutoTestClient.IS_AUTO_TEST) { - ThreadingImpl.serverCanAcceptTasks = false; - ThreadingImpl.PHASER.arriveAndDeregister(); - ThreadingImpl.isServerRunning = false; - } + ThreadingImpl.serverCanAcceptTasks = false; + ThreadingImpl.PHASER.arriveAndDeregister(); + ThreadingImpl.isServerRunning = false; } } @Inject(method = "runServer", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/MinecraftServer;runTasksTillTickEnd()V")) private void preRunTasks(CallbackInfo ci) { - if (FabricApiAutoTestClient.IS_AUTO_TEST) { - ThreadingImpl.enterPhase(ThreadingImpl.PHASE_SERVER_TASKS); - } + ThreadingImpl.enterPhase(ThreadingImpl.PHASE_SERVER_TASKS); } @Inject(method = "runServer", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/MinecraftServer;runTasksTillTickEnd()V", shift = At.Shift.AFTER)) private void postRunTasks(CallbackInfo ci) { - if (FabricApiAutoTestClient.IS_AUTO_TEST) { - ThreadingImpl.enterPhase(ThreadingImpl.PHASE_CLIENT_TASKS); - // client tasks happen here + ThreadingImpl.enterPhase(ThreadingImpl.PHASE_CLIENT_TASKS); + // client tasks happen here - ThreadingImpl.serverCanAcceptTasks = true; - ThreadingImpl.enterPhase(ThreadingImpl.PHASE_TEST); + ThreadingImpl.serverCanAcceptTasks = true; + ThreadingImpl.enterPhase(ThreadingImpl.PHASE_TEST); - if (ThreadingImpl.testThread != null) { - while (true) { - try { - ThreadingImpl.SERVER_SEMAPHORE.acquire(); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } + if (ThreadingImpl.testThread != null) { + while (true) { + try { + ThreadingImpl.SERVER_SEMAPHORE.acquire(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } - if (ThreadingImpl.taskToRun != null) { - ThreadingImpl.taskToRun.run(); - } else { - break; - } + if (ThreadingImpl.taskToRun != null) { + ThreadingImpl.taskToRun.run(); + } else { + break; } } - - ThreadingImpl.enterPhase(ThreadingImpl.PHASE_TICK); } + + ThreadingImpl.enterPhase(ThreadingImpl.PHASE_TICK); } } diff --git a/fabric-api-base/src/testmodClient/java/net/fabricmc/fabric/test/base/client/mixin/ScreenAccessor.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/gametest/client/ScreenAccessor.java similarity index 94% rename from fabric-api-base/src/testmodClient/java/net/fabricmc/fabric/test/base/client/mixin/ScreenAccessor.java rename to fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/gametest/client/ScreenAccessor.java index fd444c43cf..7497af6df1 100644 --- a/fabric-api-base/src/testmodClient/java/net/fabricmc/fabric/test/base/client/mixin/ScreenAccessor.java +++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/gametest/client/ScreenAccessor.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.fabricmc.fabric.test.base.client.mixin; +package net.fabricmc.fabric.mixin.gametest.client; import java.util.List; diff --git a/fabric-api-base/src/testmodClient/java/net/fabricmc/fabric/test/base/client/mixin/TitleScreenAccessor.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/gametest/client/TitleScreenAccessor.java similarity index 94% rename from fabric-api-base/src/testmodClient/java/net/fabricmc/fabric/test/base/client/mixin/TitleScreenAccessor.java rename to fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/gametest/client/TitleScreenAccessor.java index 6b2696aa7b..f621101e6d 100644 --- a/fabric-api-base/src/testmodClient/java/net/fabricmc/fabric/test/base/client/mixin/TitleScreenAccessor.java +++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/gametest/client/TitleScreenAccessor.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.fabricmc.fabric.test.base.client.mixin; +package net.fabricmc.fabric.mixin.gametest.client; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.gen.Accessor; diff --git a/fabric-client-gametest-api-v1/src/client/resources/assets/fabric-client-gametest-api-v1/icon.png b/fabric-client-gametest-api-v1/src/client/resources/assets/fabric-client-gametest-api-v1/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..2931efbf610873c0084debb8690902b0103d27fe GIT binary patch literal 1579 zcmbVMTWB0r7@iGm)TAXsYw<=rnU=;v=W=GRbL=!tc4Brl6GO7t2vVJ$IlDV#XU;e? z+r2ymsZdMQqAyaFLLUo;RumtE8Z@?uf_*4nP^4;o6fOFoSkN+o1$K?f2nE9_*b5G-l)AV)k5Qhb^- zU{V4ZnTKgnmXdpcB*Kg!W(1hvM2N&RO30x1u~eI9meGQGe@_?PDQq%q1CiV$8~M7 z?MQ_mOdqCh^a65Sv|ntwSXjV5se1;VK1|Kr8G7TQoQL&*ctt{L{fClG}xPK5k^yK3%T69N6J=>3jBqc zDNvZsrJ-yOXI^^mWf1cmY^XST)CVzIGjvEPENowmy}ax zvJ8_(Cf#+H-dBlH53`_u-~6BVAMz|(g?jCVdBWNZ(+A}(pFV7>S3QgPiQcMaflkIC z-3Ti|VT~{au*vq0ts9O&m$p&Gl=L6+q6_m$IcVq}o~+Pl{g>1esQp4%wp~|*zk1n` zZ7T6Toc4`y88s}riCP|ZXrJ?FLz@^KTcyqLjey zu95Yz%F&S{<0~f)Iomek?+hQ%MhCu%T^zsg>C_L`1`Br`xNY&))k9yTQb$JC>)w_f zpU(^tu^Q)y%W~lVz`jz;_ jF?g&s@Y=Qe&c#kW|JbvqK0Y=Rw)4XDoVqsk_>;c_`@;F@ literal 0 HcmV?d00001 diff --git a/fabric-api-base/src/testmodClient/resources/fabric-api-base-testmod.client.mixins.json b/fabric-client-gametest-api-v1/src/client/resources/fabric-client-gametest-api-v1.mixins.json similarity index 68% rename from fabric-api-base/src/testmodClient/resources/fabric-api-base-testmod.client.mixins.json rename to fabric-client-gametest-api-v1/src/client/resources/fabric-client-gametest-api-v1.mixins.json index fb5cc068d0..e7d83cfb49 100644 --- a/fabric-api-base/src/testmodClient/resources/fabric-api-base-testmod.client.mixins.json +++ b/fabric-client-gametest-api-v1/src/client/resources/fabric-client-gametest-api-v1.mixins.json @@ -1,18 +1,19 @@ { "required": true, - "package": "net.fabricmc.fabric.test.base.client.mixin", + "package": "net.fabricmc.fabric.mixin.gametest.client", "compatibilityLevel": "JAVA_21", - "client": [ - "CyclingButtonWidgetAccessor", - "MinecraftClientMixin", + "mixins": [ "MinecraftDedicatedServerMixin", - "ScreenAccessor", - "TitleScreenAccessor" + "MinecraftServerMixin" ], + "plugin": "net.fabricmc.fabric.impl.gametest.client.ClientGameTestMixinConfigPlugin", "injectors": { "defaultRequire": 1 }, - "mixins": [ - "MinecraftServerMixin" + "client": [ + "CyclingButtonWidgetAccessor", + "MinecraftClientMixin", + "ScreenAccessor", + "TitleScreenAccessor" ] } diff --git a/fabric-client-gametest-api-v1/src/client/resources/fabric.mod.json b/fabric-client-gametest-api-v1/src/client/resources/fabric.mod.json new file mode 100644 index 0000000000..66b17600a4 --- /dev/null +++ b/fabric-client-gametest-api-v1/src/client/resources/fabric.mod.json @@ -0,0 +1,29 @@ +{ + "schemaVersion": 1, + "id": "fabric-client-gametest-api-v1", + "name": "Fabric Client Game Test API (v1)", + "version": "${version}", + "environment": "client", + "license": "Apache-2.0", + "icon": "assets/fabric-client-gametest-api-v1/icon.png", + "contact": { + "homepage": "https://fabricmc.net", + "irc": "irc://irc.esper.net:6667/fabric", + "issues": "https://github.com/FabricMC/fabric/issues", + "sources": "https://github.com/FabricMC/fabric" + }, + "authors": [ + "FabricMC" + ], + "depends": { + "fabricloader": ">=0.16.9", + "fabric-resource-loader-v0": "*" + }, + "description": "Allows registration of client game tests.", + "mixins": [ + "fabric-client-gametest-api-v1.mixins.json" + ], + "custom": { + "fabric-api:module-lifecycle": "experimental" + } +} diff --git a/fabric-api-base/src/testmodClient/java/net/fabricmc/fabric/test/base/client/FabricApiAutoTestClient.java b/fabric-client-gametest-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/gametest/client/ClientGameTestTestMod.java similarity index 72% rename from fabric-api-base/src/testmodClient/java/net/fabricmc/fabric/test/base/client/FabricApiAutoTestClient.java rename to fabric-client-gametest-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/gametest/client/ClientGameTestTestMod.java index 60de59e2a3..aafa68b94e 100644 --- a/fabric-api-base/src/testmodClient/java/net/fabricmc/fabric/test/base/client/FabricApiAutoTestClient.java +++ b/fabric-client-gametest-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/gametest/client/ClientGameTestTestMod.java @@ -14,22 +14,22 @@ * limitations under the License. */ -package net.fabricmc.fabric.test.base.client; - -import static net.fabricmc.fabric.test.base.client.FabricClientTestHelper.clickScreenButton; -import static net.fabricmc.fabric.test.base.client.FabricClientTestHelper.closeScreen; -import static net.fabricmc.fabric.test.base.client.FabricClientTestHelper.computeOnClient; -import static net.fabricmc.fabric.test.base.client.FabricClientTestHelper.connectToServer; -import static net.fabricmc.fabric.test.base.client.FabricClientTestHelper.enableDebugHud; -import static net.fabricmc.fabric.test.base.client.FabricClientTestHelper.openGameMenu; -import static net.fabricmc.fabric.test.base.client.FabricClientTestHelper.openInventory; -import static net.fabricmc.fabric.test.base.client.FabricClientTestHelper.setPerspective; -import static net.fabricmc.fabric.test.base.client.FabricClientTestHelper.takeScreenshot; -import static net.fabricmc.fabric.test.base.client.FabricClientTestHelper.waitForLoadingComplete; -import static net.fabricmc.fabric.test.base.client.FabricClientTestHelper.waitForScreen; -import static net.fabricmc.fabric.test.base.client.FabricClientTestHelper.waitForServerStop; -import static net.fabricmc.fabric.test.base.client.FabricClientTestHelper.waitForTitleScreenFade; -import static net.fabricmc.fabric.test.base.client.FabricClientTestHelper.waitForWorldTicks; +package net.fabricmc.fabric.test.gametest.client; + +import static net.fabricmc.fabric.impl.gametest.client.FabricClientTestHelper.clickScreenButton; +import static net.fabricmc.fabric.impl.gametest.client.FabricClientTestHelper.closeScreen; +import static net.fabricmc.fabric.impl.gametest.client.FabricClientTestHelper.computeOnClient; +import static net.fabricmc.fabric.impl.gametest.client.FabricClientTestHelper.connectToServer; +import static net.fabricmc.fabric.impl.gametest.client.FabricClientTestHelper.enableDebugHud; +import static net.fabricmc.fabric.impl.gametest.client.FabricClientTestHelper.openGameMenu; +import static net.fabricmc.fabric.impl.gametest.client.FabricClientTestHelper.openInventory; +import static net.fabricmc.fabric.impl.gametest.client.FabricClientTestHelper.setPerspective; +import static net.fabricmc.fabric.impl.gametest.client.FabricClientTestHelper.takeScreenshot; +import static net.fabricmc.fabric.impl.gametest.client.FabricClientTestHelper.waitForLoadingComplete; +import static net.fabricmc.fabric.impl.gametest.client.FabricClientTestHelper.waitForScreen; +import static net.fabricmc.fabric.impl.gametest.client.FabricClientTestHelper.waitForServerStop; +import static net.fabricmc.fabric.impl.gametest.client.FabricClientTestHelper.waitForTitleScreenFade; +import static net.fabricmc.fabric.impl.gametest.client.FabricClientTestHelper.waitForWorldTicks; import java.io.IOException; import java.io.UncheckedIOException; @@ -51,14 +51,16 @@ import net.minecraft.client.option.Perspective; import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.fabric.impl.gametest.client.TestDedicatedServer; +import net.fabricmc.fabric.impl.gametest.client.ThreadingImpl; import net.fabricmc.loader.api.FabricLoader; -public class FabricApiAutoTestClient implements ClientModInitializer { - public static final boolean IS_AUTO_TEST = System.getProperty("fabric.autoTest") != null; +public class ClientGameTestTestMod implements ClientModInitializer { + public static final boolean ENABLED = System.getProperty("fabric.client.gametest") != null; @Override public void onInitializeClient() { - if (!IS_AUTO_TEST) { + if (!ENABLED) { return; } diff --git a/fabric-client-gametest-api-v1/src/testmodClient/resources/fabric.mod.json b/fabric-client-gametest-api-v1/src/testmodClient/resources/fabric.mod.json new file mode 100644 index 0000000000..c2f5ca1eb0 --- /dev/null +++ b/fabric-client-gametest-api-v1/src/testmodClient/resources/fabric.mod.json @@ -0,0 +1,13 @@ +{ + "schemaVersion": 1, + "id": "fabric-client-gametest-api-v1-testmod", + "name": "Fabric Client Game Test API (v1) Test Mod", + "version": "1.0.0", + "environment": "*", + "license": "Apache-2.0", + "entrypoints": { + "client": [ + "net.fabricmc.fabric.test.gametest.client.ClientGameTestTestMod" + ] + } +} diff --git a/gradle.properties b/gradle.properties index a506d15251..e3857cb064 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,6 +17,7 @@ fabric-biome-api-v1-version=15.0.4 fabric-block-api-v1-version=1.0.31 fabric-block-view-api-v2-version=1.0.19 fabric-blockrenderlayer-v1-version=2.0.7 +fabric-client-gametest-api-v1-version=0.0.1 fabric-command-api-v1-version=1.2.61 fabric-command-api-v2-version=2.2.40 fabric-commands-v0-version=0.2.78 diff --git a/settings.gradle b/settings.gradle index 4e3df4ebb1..150664dc37 100644 --- a/settings.gradle +++ b/settings.gradle @@ -30,6 +30,7 @@ include 'fabric-biome-api-v1' include 'fabric-block-api-v1' include 'fabric-block-view-api-v2' include 'fabric-blockrenderlayer-v1' +include 'fabric-client-gametest-api-v1' include 'fabric-client-tags-api-v1' include 'fabric-command-api-v2' include 'fabric-content-registries-v0' From 4ea71650ccce7f2eb2bfd0fd15ce708dd69f2596 Mon Sep 17 00:00:00 2001 From: modmuss50 Date: Tue, 10 Dec 2024 15:12:20 +0000 Subject: [PATCH 02/15] Fix run prod Gradle tasks --- build.gradle | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/build.gradle b/build.gradle index b9b44f79c8..a00199f6bd 100644 --- a/build.gradle +++ b/build.gradle @@ -484,6 +484,9 @@ configurations { extendsFrom configurations.minecraftRuntimeLibraries } productionRuntimeServer + productionMods { + transitive = false + } } dependencies { @@ -491,16 +494,16 @@ dependencies { productionRuntime "net.fabricmc:intermediary:${project.minecraft_version}" productionRuntimeServer "net.fabricmc:fabric-installer:${project.installer_version}:server" + productionMods project(':fabric-client-gametest-api-v1') } import net.fabricmc.loom.util.Platform +def productionMods = project.files(configurations.productionMods, remapJar.archiveFile, remapTestmodJar.archiveFile) + // This is very far beyond loom's API if you copy this, you're on your own. tasks.register('runProductionAutoTestClient', JavaExec) { - dependsOn remapJar, remapTestmodJar, downloadAssets - afterEvaluate { - dependsOn project(':fabric-client-gametest-api-v1').remapJar - } + dependsOn productionMods, downloadAssets classpath.from configurations.productionRuntime mainClass = "net.fabricmc.loader.impl.launch.knot.KnotClient" workingDir = file("run") @@ -521,11 +524,9 @@ tasks.register('runProductionAutoTestClient', JavaExec) { ) } - afterEvaluate { - jvmArgs "-Dfabric.addMods=${remapJar.archiveFile.get().asFile.absolutePath}${File.pathSeparator}${remapTestmodJar.archiveFile.get().asFile.absolutePath}${File.pathSeparator}${project(':fabric-client-gametest-api-v1').remapJar.archiveFile.get().asFile.absolutePath}" - } jvmArgs( - "-Dfabric.gametest.client", + "-Dfabric.addMods=${productionMods.collect { it.absolutePath }.join(File.pathSeparator)}", + "-Dfabric.client.gametest", "-Dfabric-tag-conventions-v2.missingTagTranslationWarning=fail", "-Dfabric-tag-conventions-v1.legacyTagWarning=fail" ) @@ -549,7 +550,7 @@ tasks.register('serverPropertiesJar', Jar) { } tasks.register('runProductionAutoTestServer', JavaExec) { - dependsOn remapJar, remapTestmodJar, serverPropertiesJar + dependsOn productionMods, serverPropertiesJar classpath.from configurations.productionRuntimeServer, serverPropertiesJar mainClass = "net.fabricmc.installer.ServerLauncher" workingDir = file("run") @@ -558,7 +559,7 @@ tasks.register('runProductionAutoTestServer', JavaExec) { workingDir.mkdirs() jvmArgs( - "-Dfabric.addMods=${remapJar.archiveFile.get().asFile.absolutePath}${File.pathSeparator}${remapTestmodJar.archiveFile.get().asFile.absolutePath}", + "-Dfabric.addMods=${productionMods.collect { it.absolutePath }.join(File.pathSeparator)}", "-Dfabric.autoTest", ) jvmArgs(debugArgs) From 7ff6520aa3ef31b3b043f3805e665eaba5ebd351 Mon Sep 17 00:00:00 2001 From: joe Date: Tue, 10 Dec 2024 16:32:29 +0000 Subject: [PATCH 03/15] Rename remaining references to client auto-tests --- .github/workflows/build.yml | 2 +- build.gradle | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 789e2640f6..4422c2ee35 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -46,7 +46,7 @@ jobs: - name: Run Auto test Client uses: modmuss50/xvfb-action@v1 with: - run: ./gradlew runProductionAutoTestClient --stacktrace --warning-mode=fail + run: ./gradlew runProductionClientGametest --stacktrace --warning-mode=fail - uses: actions/upload-artifact@v4 if: always() with: diff --git a/build.gradle b/build.gradle index a00199f6bd..24cfcb4c21 100644 --- a/build.gradle +++ b/build.gradle @@ -417,9 +417,9 @@ loom { name "Auto Test Server" vmArg "-Dfabric.autoTest" } - autoTestClient { + clientGametest { inherit testmodClient - name "Auto Test Client" + name "Client Game Test" vmArg "-Dfabric.client.gametest" vmArg "-Dfabric-tag-conventions-v2.missingTagTranslationWarning=fail" vmArg "-Dfabric-tag-conventions-v1.legacyTagWarning=fail" @@ -432,9 +432,9 @@ loom { ideConfigGenerated = false } - autoTestClientCoverage { - inherit autoTestClient - name "Auto Test Client Coverage" + clientGametestCoverage { + inherit clientGametest + name "Client Game Test Coverage" ideConfigGenerated = false } } @@ -448,7 +448,7 @@ test.dependsOn runGametest def coverageTasks = [ runGametestCoverage, - runAutoTestClientCoverage + runClientGametestCoverage ] jacoco { @@ -502,7 +502,7 @@ import net.fabricmc.loom.util.Platform def productionMods = project.files(configurations.productionMods, remapJar.archiveFile, remapTestmodJar.archiveFile) // This is very far beyond loom's API if you copy this, you're on your own. -tasks.register('runProductionAutoTestClient', JavaExec) { +tasks.register('runProductionClientGametest', JavaExec) { dependsOn productionMods, downloadAssets classpath.from configurations.productionRuntime mainClass = "net.fabricmc.loader.impl.launch.knot.KnotClient" From bbe2f363788abc07708fff6d128bdc845482006e Mon Sep 17 00:00:00 2001 From: joe Date: Tue, 10 Dec 2024 20:01:06 +0000 Subject: [PATCH 04/15] Switch client gametests to using entrypoints --- .../gametest/v1/ClientGameTestContext.java | 41 ++++++ .../gametest/v1/FabricClientGameTest.java | 27 ++++ .../api/client/gametest/v1/package-info.java | 25 ++++ .../gametest/ClientGameTestContextImpl.java | 138 ++++++++++++++++++ .../ClientGameTestMixinConfigPlugin.java | 2 +- .../gametest/FabricClientGameTestRunner.java | 61 ++++++++ .../gametest}/FabricClientTestHelper.java | 8 +- .../gametest}/TestDedicatedServer.java | 2 +- .../gametest}/ThreadingImpl.java | 2 +- .../CyclingButtonWidgetAccessor.java | 2 +- .../gametest}/MinecraftClientMixin.java | 22 ++- .../MinecraftDedicatedServerMixin.java | 4 +- .../gametest}/MinecraftServerMixin.java | 4 +- .../gametest}/ScreenAccessor.java | 2 +- .../gametest}/TitleScreenAccessor.java | 2 +- .../fabric-client-gametest-api-v1.mixins.json | 4 +- ...stTestMod.java => ClientGameTestTest.java} | 50 +++---- .../testmodClient/resources/fabric.mod.json | 4 +- 18 files changed, 348 insertions(+), 52 deletions(-) create mode 100644 fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/ClientGameTestContext.java create mode 100644 fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/FabricClientGameTest.java create mode 100644 fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/package-info.java create mode 100644 fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/ClientGameTestContextImpl.java rename fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/{gametest/client => client/gametest}/ClientGameTestMixinConfigPlugin.java (97%) create mode 100644 fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/FabricClientGameTestRunner.java rename fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/{gametest/client => client/gametest}/FabricClientTestHelper.java (96%) rename fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/{gametest/client => client/gametest}/TestDedicatedServer.java (98%) rename fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/{gametest/client => client/gametest}/ThreadingImpl.java (99%) rename fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/{gametest/client => client/gametest}/CyclingButtonWidgetAccessor.java (94%) rename fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/{gametest/client => client/gametest}/MinecraftClientMixin.java (88%) rename fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/{gametest/client => client/gametest}/MinecraftDedicatedServerMixin.java (94%) rename fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/{gametest/client => client/gametest}/MinecraftServerMixin.java (95%) rename fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/{gametest/client => client/gametest}/ScreenAccessor.java (94%) rename fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/{gametest/client => client/gametest}/TitleScreenAccessor.java (94%) rename fabric-client-gametest-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/gametest/client/{ClientGameTestTestMod.java => ClientGameTestTest.java} (76%) diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/ClientGameTestContext.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/ClientGameTestContext.java new file mode 100644 index 0000000000..65d5efea7b --- /dev/null +++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/ClientGameTestContext.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.api.client.gametest.v1; + +import org.apache.commons.lang3.function.FailableConsumer; +import org.apache.commons.lang3.function.FailableFunction; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.screen.Screen; + +/** + * Context for a client gametest containing various helpful functions and functions to access the game. + */ +@ApiStatus.NonExtendable +public interface ClientGameTestContext { + void waitTick(); + void waitTicks(int ticks); + + void setScreen(@Nullable Screen screen); + void clickScreenButton(String translationKey); + boolean tryClickScreenButton(String translationKey); + + void runOnClient(FailableConsumer action) throws E; + T computeOnClient(FailableFunction function) throws E; +} diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/FabricClientGameTest.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/FabricClientGameTest.java new file mode 100644 index 0000000000..f59de17b5b --- /dev/null +++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/FabricClientGameTest.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.api.client.gametest.v1; + +/** + * The client game test entrypoint interface. See the package documentation. + */ +public interface FabricClientGameTest { + /** + * Runs the gametest. + */ + void runTest(ClientGameTestContext context); +} diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/package-info.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/package-info.java new file mode 100644 index 0000000000..79497dca5d --- /dev/null +++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/package-info.java @@ -0,0 +1,25 @@ +/** + * Provides support for client gametests. To register a client gametest, add an entry to the + * {@code fabric-client-gametest} entrypoint in your {@code fabric.mod.json}. Your gametest class should implement + * {@link net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest FabricClientGameTest}. + * + *

Lifecycle

+ * Client gametests are run sequentially. When a gametest ends, the game will be + * returned to the title screen. When all gametests have been run, the game will be closed. + * + *

Threading

+ * + *

Client gametests run on the client gametest thread. Use the functions inside + * {@link net.fabricmc.fabric.api.client.gametest.v1.ClientGameTestContext ClientGameTestContext} and other test helper + * classes to run code on the correct thread. The game remains paused unless you explicitly unpause it using various + * waiting functions such as + * {@link net.fabricmc.fabric.api.client.gametest.v1.ClientGameTestContext#waitTick() ClientGameTestContext.waitTick()}. + * + *

A few changes have been made to how the vanilla game threads run, to make tests more reproducible. Notably, there + * is exactly one server tick per client tick while a server is running (singleplayer or multiplayer). On singleplayer, + * packets will always arrive on a consistent tick. + */ +@ApiStatus.Experimental +package net.fabricmc.fabric.api.client.gametest.v1; + +import org.jetbrains.annotations.ApiStatus; diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/ClientGameTestContextImpl.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/ClientGameTestContextImpl.java new file mode 100644 index 0000000000..b110b95218 --- /dev/null +++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/ClientGameTestContextImpl.java @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.impl.client.gametest; + +import com.google.common.base.Preconditions; +import org.apache.commons.lang3.function.FailableConsumer; +import org.apache.commons.lang3.function.FailableFunction; +import org.apache.commons.lang3.mutable.MutableObject; +import org.jetbrains.annotations.Nullable; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.Drawable; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.gui.widget.ClickableWidget; +import net.minecraft.client.gui.widget.CyclingButtonWidget; +import net.minecraft.client.gui.widget.PressableWidget; +import net.minecraft.client.gui.widget.Widget; +import net.minecraft.text.Text; +import net.minecraft.util.Nullables; + +import net.fabricmc.fabric.api.client.gametest.v1.ClientGameTestContext; +import net.fabricmc.fabric.mixin.client.gametest.CyclingButtonWidgetAccessor; +import net.fabricmc.fabric.mixin.client.gametest.ScreenAccessor; + +public final class ClientGameTestContextImpl implements ClientGameTestContext { + @Override + public void waitTick() { + ThreadingImpl.runTick(); + } + + @Override + public void waitTicks(int ticks) { + Preconditions.checkArgument(ticks >= 0, "ticks cannot be negative"); + + for (int i = 0; i < ticks; i++) { + ThreadingImpl.runTick(); + } + } + + @Override + public void setScreen(@Nullable Screen screen) { + runOnClient(client -> client.setScreen(screen)); + } + + @Override + public void clickScreenButton(String translationKey) { + Preconditions.checkNotNull(translationKey, "translationKey"); + + runOnClient(client -> { + if (!tryClickScreenButtonImpl(client.currentScreen, translationKey)) { + throw new AssertionError("Could not find button '%s' in screen '%s'".formatted( + translationKey, + Nullables.map(client.currentScreen, screen -> screen.getClass().getName()) + )); + } + }); + } + + @Override + public boolean tryClickScreenButton(String translationKey) { + Preconditions.checkNotNull(translationKey, "translationKey"); + + return computeOnClient(client -> tryClickScreenButtonImpl(client.currentScreen, translationKey)); + } + + private static boolean tryClickScreenButtonImpl(@Nullable Screen screen, String translationKey) { + if (screen == null) { + return false; + } + + final String buttonText = Text.translatable(translationKey).getString(); + final ScreenAccessor screenAccessor = (ScreenAccessor) screen; + + for (Drawable drawable : screenAccessor.getDrawables()) { + if (drawable instanceof PressableWidget pressableWidget && pressMatchingButton(pressableWidget, buttonText)) { + return true; + } + + if (drawable instanceof Widget widget) { + widget.forEachChild(clickableWidget -> pressMatchingButton(clickableWidget, buttonText)); + } + } + + // Was unable to find the button to press + return false; + } + + private static boolean pressMatchingButton(ClickableWidget widget, String text) { + if (widget instanceof ButtonWidget buttonWidget) { + if (text.equals(buttonWidget.getMessage().getString())) { + buttonWidget.onPress(); + return true; + } + } + + if (widget instanceof CyclingButtonWidget buttonWidget) { + CyclingButtonWidgetAccessor accessor = (CyclingButtonWidgetAccessor) buttonWidget; + + if (text.equals(accessor.getOptionText().getString())) { + buttonWidget.onPress(); + return true; + } + } + + return false; + } + + @Override + public void runOnClient(FailableConsumer action) throws E { + Preconditions.checkNotNull(action, "action"); + + ThreadingImpl.runOnClient(() -> action.accept(MinecraftClient.getInstance())); + } + + @Override + public T computeOnClient(FailableFunction function) throws E { + Preconditions.checkNotNull(function, "function"); + + MutableObject result = new MutableObject<>(); + ThreadingImpl.runOnClient(() -> result.setValue(function.apply(MinecraftClient.getInstance()))); + return result.getValue(); + } +} diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/gametest/client/ClientGameTestMixinConfigPlugin.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/ClientGameTestMixinConfigPlugin.java similarity index 97% rename from fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/gametest/client/ClientGameTestMixinConfigPlugin.java rename to fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/ClientGameTestMixinConfigPlugin.java index 79c0c68778..f0e6511dbe 100644 --- a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/gametest/client/ClientGameTestMixinConfigPlugin.java +++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/ClientGameTestMixinConfigPlugin.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.fabricmc.fabric.impl.gametest.client; +package net.fabricmc.fabric.impl.client.gametest; import java.util.List; import java.util.Set; diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/FabricClientGameTestRunner.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/FabricClientGameTestRunner.java new file mode 100644 index 0000000000..bf16be7f97 --- /dev/null +++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/FabricClientGameTestRunner.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.impl.client.gametest; + +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.minecraft.client.gui.screen.TitleScreen; + +import net.fabricmc.fabric.api.client.gametest.v1.ClientGameTestContext; +import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest; +import net.fabricmc.loader.api.FabricLoader; + +public class FabricClientGameTestRunner { + private static final Logger LOGGER = LoggerFactory.getLogger("fabric-client-gametest-api-v1"); + private static final String ENTRYPOINT_KEY = "fabric-client-gametest"; + + public static void start() { + List gameTests = FabricLoader.getInstance().getEntrypoints(ENTRYPOINT_KEY, FabricClientGameTest.class); + ThreadingImpl.runTestThread(() -> { + ClientGameTestContext context = new ClientGameTestContextImpl(); + boolean failed = false; + + for (FabricClientGameTest gameTest : gameTests) { + try { + gameTest.runTest(context); + } catch (Throwable e) { + LOGGER.error("Failed test {}", gameTest.getClass().getName(), e); + failed = true; + } finally { + // Open the title screen to reset the state for the next gametest. + // If the gametest API was used correctly, we should be in the menus somewhere because any test + // world should have been closed at the end of a try-with-resources statement. + context.setScreen(new TitleScreen()); + } + } + + if (failed) { + throw new AssertionError("There were failing client gametests"); + } + + context.clickScreenButton("menu.quit"); + }); + } +} diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/gametest/client/FabricClientTestHelper.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/FabricClientTestHelper.java similarity index 96% rename from fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/gametest/client/FabricClientTestHelper.java rename to fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/FabricClientTestHelper.java index 299663fffd..dcb7d08e1f 100644 --- a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/gametest/client/FabricClientTestHelper.java +++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/FabricClientTestHelper.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.fabricmc.fabric.impl.gametest.client; +package net.fabricmc.fabric.impl.client.gametest; import java.util.Objects; import java.util.function.Function; @@ -45,9 +45,9 @@ import net.minecraft.client.util.ScreenshotRecorder; import net.minecraft.text.Text; -import net.fabricmc.fabric.mixin.gametest.client.CyclingButtonWidgetAccessor; -import net.fabricmc.fabric.mixin.gametest.client.ScreenAccessor; -import net.fabricmc.fabric.mixin.gametest.client.TitleScreenAccessor; +import net.fabricmc.fabric.mixin.client.gametest.CyclingButtonWidgetAccessor; +import net.fabricmc.fabric.mixin.client.gametest.ScreenAccessor; +import net.fabricmc.fabric.mixin.client.gametest.TitleScreenAccessor; import net.fabricmc.loader.api.FabricLoader; public final class FabricClientTestHelper { diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/gametest/client/TestDedicatedServer.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/TestDedicatedServer.java similarity index 98% rename from fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/gametest/client/TestDedicatedServer.java rename to fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/TestDedicatedServer.java index 7cc62379c6..8d6105e4b8 100644 --- a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/gametest/client/TestDedicatedServer.java +++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/TestDedicatedServer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.fabricmc.fabric.impl.gametest.client; +package net.fabricmc.fabric.impl.client.gametest; import java.io.Closeable; import java.io.IOException; diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/gametest/client/ThreadingImpl.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/ThreadingImpl.java similarity index 99% rename from fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/gametest/client/ThreadingImpl.java rename to fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/ThreadingImpl.java index 87da7f9264..a739cbc0dc 100644 --- a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/gametest/client/ThreadingImpl.java +++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/ThreadingImpl.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.fabricmc.fabric.impl.gametest.client; +package net.fabricmc.fabric.impl.client.gametest; import java.util.concurrent.Phaser; import java.util.concurrent.Semaphore; diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/gametest/client/CyclingButtonWidgetAccessor.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/CyclingButtonWidgetAccessor.java similarity index 94% rename from fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/gametest/client/CyclingButtonWidgetAccessor.java rename to fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/CyclingButtonWidgetAccessor.java index 2b8fd4a9d6..539107be24 100644 --- a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/gametest/client/CyclingButtonWidgetAccessor.java +++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/CyclingButtonWidgetAccessor.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.fabricmc.fabric.mixin.gametest.client; +package net.fabricmc.fabric.mixin.client.gametest; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.gen.Accessor; diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/gametest/client/MinecraftClientMixin.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/MinecraftClientMixin.java similarity index 88% rename from fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/gametest/client/MinecraftClientMixin.java rename to fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/MinecraftClientMixin.java index d5ddd343cb..cf1d7ffceb 100644 --- a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/gametest/client/MinecraftClientMixin.java +++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/MinecraftClientMixin.java @@ -14,12 +14,14 @@ * limitations under the License. */ -package net.fabricmc.fabric.mixin.gametest.client; +package net.fabricmc.fabric.mixin.client.gametest; import com.google.common.base.Preconditions; import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import org.jetbrains.annotations.Nullable; import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; @@ -27,18 +29,26 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.screen.Overlay; import net.minecraft.client.gui.screen.Screen; import net.minecraft.resource.ResourcePackManager; import net.minecraft.server.SaveLoader; import net.minecraft.world.level.storage.LevelStorage; -import net.fabricmc.fabric.impl.gametest.client.ThreadingImpl; +import net.fabricmc.fabric.impl.client.gametest.FabricClientGameTestRunner; +import net.fabricmc.fabric.impl.client.gametest.ThreadingImpl; @Mixin(MinecraftClient.class) public class MinecraftClientMixin { + @Unique + private boolean startedClientGametests = false; @Unique private Runnable deferredTask = null; + @Shadow + @Nullable + private Overlay overlay; + @WrapMethod(method = "run") private void onRun(Operation original) { if (ThreadingImpl.isClientRunning) { @@ -57,6 +67,14 @@ private void onRun(Operation original) { } } + @Inject(method = "tick", at = @At("HEAD")) + private void onTick(CallbackInfo ci) { + if (!startedClientGametests && overlay == null) { + startedClientGametests = true; + FabricClientGameTestRunner.start(); + } + } + @Inject(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/MinecraftClient;runTasks()V")) private void preRunTasks(CallbackInfo ci) { ThreadingImpl.enterPhase(ThreadingImpl.PHASE_SERVER_TASKS); diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/gametest/client/MinecraftDedicatedServerMixin.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/MinecraftDedicatedServerMixin.java similarity index 94% rename from fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/gametest/client/MinecraftDedicatedServerMixin.java rename to fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/MinecraftDedicatedServerMixin.java index c5e7ece359..19575f6069 100644 --- a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/gametest/client/MinecraftDedicatedServerMixin.java +++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/MinecraftDedicatedServerMixin.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.fabricmc.fabric.mixin.gametest.client; +package net.fabricmc.fabric.mixin.client.gametest; import com.llamalad7.mixinextras.injector.wrapoperation.Operation; import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; @@ -25,7 +25,7 @@ import net.minecraft.server.dedicated.MinecraftDedicatedServer; -import net.fabricmc.fabric.impl.gametest.client.TestDedicatedServer; +import net.fabricmc.fabric.impl.client.gametest.TestDedicatedServer; @Mixin(MinecraftDedicatedServer.class) public abstract class MinecraftDedicatedServerMixin { diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/gametest/client/MinecraftServerMixin.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/MinecraftServerMixin.java similarity index 95% rename from fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/gametest/client/MinecraftServerMixin.java rename to fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/MinecraftServerMixin.java index 702cd87efe..3f6c0e827f 100644 --- a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/gametest/client/MinecraftServerMixin.java +++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/MinecraftServerMixin.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.fabricmc.fabric.mixin.gametest.client; +package net.fabricmc.fabric.mixin.client.gametest; import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; import com.llamalad7.mixinextras.injector.wrapoperation.Operation; @@ -25,7 +25,7 @@ import net.minecraft.server.MinecraftServer; -import net.fabricmc.fabric.impl.gametest.client.ThreadingImpl; +import net.fabricmc.fabric.impl.client.gametest.ThreadingImpl; @Mixin(MinecraftServer.class) public class MinecraftServerMixin { diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/gametest/client/ScreenAccessor.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/ScreenAccessor.java similarity index 94% rename from fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/gametest/client/ScreenAccessor.java rename to fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/ScreenAccessor.java index 7497af6df1..b0236a09ed 100644 --- a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/gametest/client/ScreenAccessor.java +++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/ScreenAccessor.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.fabricmc.fabric.mixin.gametest.client; +package net.fabricmc.fabric.mixin.client.gametest; import java.util.List; diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/gametest/client/TitleScreenAccessor.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/TitleScreenAccessor.java similarity index 94% rename from fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/gametest/client/TitleScreenAccessor.java rename to fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/TitleScreenAccessor.java index f621101e6d..654a0092f4 100644 --- a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/gametest/client/TitleScreenAccessor.java +++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/TitleScreenAccessor.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.fabricmc.fabric.mixin.gametest.client; +package net.fabricmc.fabric.mixin.client.gametest; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.gen.Accessor; diff --git a/fabric-client-gametest-api-v1/src/client/resources/fabric-client-gametest-api-v1.mixins.json b/fabric-client-gametest-api-v1/src/client/resources/fabric-client-gametest-api-v1.mixins.json index e7d83cfb49..f8521bfacf 100644 --- a/fabric-client-gametest-api-v1/src/client/resources/fabric-client-gametest-api-v1.mixins.json +++ b/fabric-client-gametest-api-v1/src/client/resources/fabric-client-gametest-api-v1.mixins.json @@ -1,12 +1,12 @@ { "required": true, - "package": "net.fabricmc.fabric.mixin.gametest.client", + "package": "net.fabricmc.fabric.mixin.client.gametest", "compatibilityLevel": "JAVA_21", "mixins": [ "MinecraftDedicatedServerMixin", "MinecraftServerMixin" ], - "plugin": "net.fabricmc.fabric.impl.gametest.client.ClientGameTestMixinConfigPlugin", + "plugin": "net.fabricmc.fabric.impl.client.gametest.ClientGameTestMixinConfigPlugin", "injectors": { "defaultRequire": 1 }, diff --git a/fabric-client-gametest-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/gametest/client/ClientGameTestTestMod.java b/fabric-client-gametest-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/gametest/client/ClientGameTestTest.java similarity index 76% rename from fabric-client-gametest-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/gametest/client/ClientGameTestTestMod.java rename to fabric-client-gametest-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/gametest/client/ClientGameTestTest.java index aafa68b94e..361aa3ac4b 100644 --- a/fabric-client-gametest-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/gametest/client/ClientGameTestTestMod.java +++ b/fabric-client-gametest-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/gametest/client/ClientGameTestTest.java @@ -16,20 +16,19 @@ package net.fabricmc.fabric.test.gametest.client; -import static net.fabricmc.fabric.impl.gametest.client.FabricClientTestHelper.clickScreenButton; -import static net.fabricmc.fabric.impl.gametest.client.FabricClientTestHelper.closeScreen; -import static net.fabricmc.fabric.impl.gametest.client.FabricClientTestHelper.computeOnClient; -import static net.fabricmc.fabric.impl.gametest.client.FabricClientTestHelper.connectToServer; -import static net.fabricmc.fabric.impl.gametest.client.FabricClientTestHelper.enableDebugHud; -import static net.fabricmc.fabric.impl.gametest.client.FabricClientTestHelper.openGameMenu; -import static net.fabricmc.fabric.impl.gametest.client.FabricClientTestHelper.openInventory; -import static net.fabricmc.fabric.impl.gametest.client.FabricClientTestHelper.setPerspective; -import static net.fabricmc.fabric.impl.gametest.client.FabricClientTestHelper.takeScreenshot; -import static net.fabricmc.fabric.impl.gametest.client.FabricClientTestHelper.waitForLoadingComplete; -import static net.fabricmc.fabric.impl.gametest.client.FabricClientTestHelper.waitForScreen; -import static net.fabricmc.fabric.impl.gametest.client.FabricClientTestHelper.waitForServerStop; -import static net.fabricmc.fabric.impl.gametest.client.FabricClientTestHelper.waitForTitleScreenFade; -import static net.fabricmc.fabric.impl.gametest.client.FabricClientTestHelper.waitForWorldTicks; +import static net.fabricmc.fabric.impl.client.gametest.FabricClientTestHelper.clickScreenButton; +import static net.fabricmc.fabric.impl.client.gametest.FabricClientTestHelper.closeScreen; +import static net.fabricmc.fabric.impl.client.gametest.FabricClientTestHelper.computeOnClient; +import static net.fabricmc.fabric.impl.client.gametest.FabricClientTestHelper.connectToServer; +import static net.fabricmc.fabric.impl.client.gametest.FabricClientTestHelper.enableDebugHud; +import static net.fabricmc.fabric.impl.client.gametest.FabricClientTestHelper.openGameMenu; +import static net.fabricmc.fabric.impl.client.gametest.FabricClientTestHelper.openInventory; +import static net.fabricmc.fabric.impl.client.gametest.FabricClientTestHelper.setPerspective; +import static net.fabricmc.fabric.impl.client.gametest.FabricClientTestHelper.takeScreenshot; +import static net.fabricmc.fabric.impl.client.gametest.FabricClientTestHelper.waitForScreen; +import static net.fabricmc.fabric.impl.client.gametest.FabricClientTestHelper.waitForServerStop; +import static net.fabricmc.fabric.impl.client.gametest.FabricClientTestHelper.waitForTitleScreenFade; +import static net.fabricmc.fabric.impl.client.gametest.FabricClientTestHelper.waitForWorldTicks; import java.io.IOException; import java.io.UncheckedIOException; @@ -50,26 +49,13 @@ import net.minecraft.client.gui.screen.world.SelectWorldScreen; import net.minecraft.client.option.Perspective; -import net.fabricmc.api.ClientModInitializer; -import net.fabricmc.fabric.impl.gametest.client.TestDedicatedServer; -import net.fabricmc.fabric.impl.gametest.client.ThreadingImpl; +import net.fabricmc.fabric.api.client.gametest.v1.ClientGameTestContext; +import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest; +import net.fabricmc.fabric.impl.client.gametest.TestDedicatedServer; import net.fabricmc.loader.api.FabricLoader; -public class ClientGameTestTestMod implements ClientModInitializer { - public static final boolean ENABLED = System.getProperty("fabric.client.gametest") != null; - - @Override - public void onInitializeClient() { - if (!ENABLED) { - return; - } - - ThreadingImpl.runTestThread(this::runTest); - } - - private void runTest() { - waitForLoadingComplete(); - +public class ClientGameTestTest implements FabricClientGameTest { + public void runTest(ClientGameTestContext context) { final boolean onboardAccessibility = computeOnClient(client -> client.options.onboardAccessibility); if (onboardAccessibility) { diff --git a/fabric-client-gametest-api-v1/src/testmodClient/resources/fabric.mod.json b/fabric-client-gametest-api-v1/src/testmodClient/resources/fabric.mod.json index c2f5ca1eb0..e1e761f4bb 100644 --- a/fabric-client-gametest-api-v1/src/testmodClient/resources/fabric.mod.json +++ b/fabric-client-gametest-api-v1/src/testmodClient/resources/fabric.mod.json @@ -6,8 +6,8 @@ "environment": "*", "license": "Apache-2.0", "entrypoints": { - "client": [ - "net.fabricmc.fabric.test.gametest.client.ClientGameTestTestMod" + "fabric-client-gametest": [ + "net.fabricmc.fabric.test.gametest.client.ClientGameTestTest" ] } } From 86077720cce73f5a7576fc2516900c19868a1542 Mon Sep 17 00:00:00 2001 From: joe Date: Tue, 10 Dec 2024 21:19:01 +0000 Subject: [PATCH 05/15] Disable input and cursor grabbing --- .../mixin/client/gametest/InputUtilMixin.java | 43 +++++++++++++++++++ .../fabric-client-gametest-api-v1.mixins.json | 1 + 2 files changed, 44 insertions(+) create mode 100644 fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/InputUtilMixin.java diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/InputUtilMixin.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/InputUtilMixin.java new file mode 100644 index 0000000000..737be7af43 --- /dev/null +++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/InputUtilMixin.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.mixin.client.gametest; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import net.minecraft.client.util.InputUtil; + +@Mixin(InputUtil.class) +public class InputUtilMixin { + @Inject(method = "isKeyPressed", at = @At("HEAD"), cancellable = true) + private static void keysNeverPressed(CallbackInfoReturnable cir) { + cir.setReturnValue(false); + } + + @Inject(method = {"setKeyboardCallbacks", "setMouseCallbacks"}, at = @At("HEAD"), cancellable = true) + private static void dontAttachCallbacks(CallbackInfo ci) { + ci.cancel(); + } + + @Inject(method = "setCursorParameters", at = @At("HEAD"), cancellable = true) + private static void disableCursorLocking(CallbackInfo ci) { + ci.cancel(); + } +} diff --git a/fabric-client-gametest-api-v1/src/client/resources/fabric-client-gametest-api-v1.mixins.json b/fabric-client-gametest-api-v1/src/client/resources/fabric-client-gametest-api-v1.mixins.json index f8521bfacf..3b10f16fa1 100644 --- a/fabric-client-gametest-api-v1/src/client/resources/fabric-client-gametest-api-v1.mixins.json +++ b/fabric-client-gametest-api-v1/src/client/resources/fabric-client-gametest-api-v1.mixins.json @@ -12,6 +12,7 @@ }, "client": [ "CyclingButtonWidgetAccessor", + "InputUtilMixin", "MinecraftClientMixin", "ScreenAccessor", "TitleScreenAccessor" From 7140a4fafcdca538c65645e495e6dc38b89137c8 Mon Sep 17 00:00:00 2001 From: joe Date: Wed, 11 Dec 2024 02:22:13 +0000 Subject: [PATCH 06/15] Remove FabricClientTestHelper moving most of it into the context. Add the ability to simulate key and mouse inputs --- fabric-client-gametest-api-v1/build.gradle | 4 + .../gametest/v1/ClientGameTestContext.java | 116 +++++- .../gametest/v1/ClientGameTestInput.java | 93 +++++ .../gametest/ClientGameTestContextImpl.java | 176 +++++++++ .../gametest/ClientGameTestInputImpl.java | 333 ++++++++++++++++++ .../gametest/FabricClientGameTestRunner.java | 12 +- .../gametest/FabricClientTestHelper.java | 226 ------------ .../impl/client/gametest/ThreadingImpl.java | 16 +- .../client/gametest/GameOptionsAccessor.java | 28 ++ .../client/gametest/GameOptionsMixin.java | 34 ++ .../mixin/client/gametest/InputUtilMixin.java | 6 +- .../client/gametest/KeyBindingAccessor.java | 29 ++ .../client/gametest/KeyboardAccessor.java | 28 ++ .../client/gametest/MinecraftClientMixin.java | 6 +- .../mixin/client/gametest/MouseAccessor.java | 34 ++ .../mixin/client/gametest/WindowMixin.java | 32 ++ ...abric-client-gametest-api-v1.accesswidener | 2 + .../fabric-client-gametest-api-v1.mixins.json | 8 +- .../src/client/resources/fabric.mod.json | 1 + .../gametest/client/ClientGameTestTest.java | 155 ++++---- 20 files changed, 1034 insertions(+), 305 deletions(-) create mode 100644 fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/ClientGameTestInput.java create mode 100644 fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/ClientGameTestInputImpl.java delete mode 100644 fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/FabricClientTestHelper.java create mode 100644 fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/GameOptionsAccessor.java create mode 100644 fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/GameOptionsMixin.java create mode 100644 fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/KeyBindingAccessor.java create mode 100644 fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/KeyboardAccessor.java create mode 100644 fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/MouseAccessor.java create mode 100644 fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/WindowMixin.java create mode 100644 fabric-client-gametest-api-v1/src/client/resources/fabric-client-gametest-api-v1.accesswidener diff --git a/fabric-client-gametest-api-v1/build.gradle b/fabric-client-gametest-api-v1/build.gradle index f2db1b51aa..c15dd4930d 100644 --- a/fabric-client-gametest-api-v1/build.gradle +++ b/fabric-client-gametest-api-v1/build.gradle @@ -1,5 +1,9 @@ version = getSubprojectVersion(project) +loom { + accessWidenerPath = file('src/client/resources/fabric-client-gametest-api-v1.accesswidener') +} + moduleDependencies(project, [ 'fabric-api-base', 'fabric-resource-loader-v0' diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/ClientGameTestContext.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/ClientGameTestContext.java index 65d5efea7b..c02f961c4b 100644 --- a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/ClientGameTestContext.java +++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/ClientGameTestContext.java @@ -16,26 +16,140 @@ package net.fabricmc.fabric.api.client.gametest.v1; +import java.util.function.Predicate; + import org.apache.commons.lang3.function.FailableConsumer; import org.apache.commons.lang3.function.FailableFunction; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; +import net.minecraft.SharedConstants; import net.minecraft.client.MinecraftClient; import net.minecraft.client.gui.screen.Screen; /** - * Context for a client gametest containing various helpful functions and functions to access the game. + * Context for a client gametest containing various helpful functions and functions to access the game. Functions in + * this class can only be called on the client gametest thread. */ @ApiStatus.NonExtendable public interface ClientGameTestContext { + /** + * Used to specify that a wait task should have no timeout. + */ + int NO_TIMEOUT = -1; + + /** + * The default timeout for wait tasks. + */ + int DEFAULT_TIMEOUT = 10 * SharedConstants.TICKS_PER_SECOND; + + /** + * Runs a single tick and waits for it to complete. + */ void waitTick(); + + /** + * Runs {@code ticks} ticks and waits for them to complete. + * + * @param ticks The amount of ticks to run + */ void waitTicks(int ticks); + /** + * Waits for a predicate to be true. Fails if the predicate is not satisfied after {@link #DEFAULT_TIMEOUT} ticks. + * + * @param predicate The predicate to check + */ + void waitFor(Predicate predicate); + + /** + * Waits for a predicate to be true. Fails if the predicate is not satisfied after {@code timeout} ticks. If + * {@code timeout} is {@link #NO_TIMEOUT}, there is no timeout. + * + * @param predicate The predicate to check + * @param timeout The number of ticks before timing out + */ + void waitFor(Predicate predicate, int timeout); + + /** + * Waits for the given screen class to be shown. If {@code screenClass} is {@code null}, waits for the current + * screen to be {@code null}. Fails if the screen does not open after {@link #DEFAULT_TIMEOUT} ticks. + * + * @param screenClass The screen class to wait to open + */ + void waitForScreen(@Nullable Class screenClass); + + /** + * Opens a {@link Screen} on the client. + * + * @param screen The screen to open + * @see MinecraftClient#setScreen(Screen) + */ void setScreen(@Nullable Screen screen); + + /** + * Presses the button in the current screen whose label is the given translation key. Fails if the button couldn't + * be found. + * + * @param translationKey The translation key of the label of the button to press + */ void clickScreenButton(String translationKey); + + /** + * Presses the button in the current screen whose label is the given translation key, if the button exists. Returns + * whether the button was found. + * + * @param translationKey The translation key of the label of the button to press + * @return Whether the button was found + */ boolean tryClickScreenButton(String translationKey); + /** + * Takes a screenshot after waiting 1 tick (for a frame to render) and saves it in the screenshots directory. + * + * @param name The name of the screenshot + */ + void takeScreenshot(String name); + + /** + * Takes a screnshot after waiting {@code delay} ticks and saves it in the screenshots directory. + * + * @param name The name of the screenshot + * @param delay The delay in ticks before taking the screenshot + */ + void takeScreenshot(String name, int delay); + + /** + * Gets the input handler used to simulate inputs to the client. + * + * @return The client gametest input handler + */ + ClientGameTestInput getInput(); + + /** + * Restores all game options in {@link MinecraftClient#options} to their default values for client gametests. This + * is called automatically before each gametest is run, so you only need to call this explicitly if you want to do + * it in the middle of the test. + */ + void restoreDefaultGameOptions(); + + /** + * Runs the given action on the render thread (client thread), and waits for it to complete. + * + * @param action The action to run on the render thread + * @param The type of checked exception that the action throws + * @throws E When the action throws an exception + */ void runOnClient(FailableConsumer action) throws E; + + /** + * Runs the given function on the render thread (client thread), and returns the result. + * + * @param function The function to run on the render thread + * @return The result of the function + * @param The type of the value to return + * @param The type of the checked exception that the function throws + * @throws E When the function throws an exception + */ T computeOnClient(FailableFunction function) throws E; } diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/ClientGameTestInput.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/ClientGameTestInput.java new file mode 100644 index 0000000000..b83d099280 --- /dev/null +++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/ClientGameTestInput.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.api.client.gametest.v1; + +import java.util.function.Function; + +import net.minecraft.client.option.GameOptions; +import net.minecraft.client.option.KeyBinding; +import net.minecraft.client.util.InputUtil; + +/** + * The client gametest input handler used to simulate inputs to the client. + */ +public interface ClientGameTestInput { + // TODO: document all these methods + void pressKey(KeyBinding keyBinding); + + void pressKey(Function keyBindingGetter); + + void pressKey(InputUtil.Key key); + + void pressKey(int keyCode); + + void pressMouse(int button); + + void pressControl(); + + void pressShift(); + + void pressAlt(); + + void releaseKey(KeyBinding keyBinding); + + void releaseKey(Function keyBindingGetter); + + void releaseKey(InputUtil.Key key); + + void releaseKey(int keyCode); + + void releaseMouse(int button); + + void releaseControl(); + + void releaseShift(); + + void releaseAlt(); + + void pressReleaseKey(KeyBinding keyBinding); + + void pressReleaseKey(Function keyBindingGetter); + + void pressReleaseKey(InputUtil.Key key); + + void pressReleaseKey(int keyCode); + + void pressReleaseMouse(int button); + + void holdKey(KeyBinding keyBinding, int ticks); + + void holdKey(Function keyBindingGetter, int ticks); + + void holdKey(InputUtil.Key key, int ticks); + + void holdKey(int keyCode, int ticks); + + void holdMouse(int button, int ticks); + + void typeChar(int codePoint); + + void typeChars(String chars); + + void scroll(double amount); + + void scroll(double xAmount, double yAmount); + + void setCursorPos(double x, double y); + + void moveCursor(double deltaX, double deltaY); +} diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/ClientGameTestContextImpl.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/ClientGameTestContextImpl.java index b110b95218..8dfaaa2266 100644 --- a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/ClientGameTestContextImpl.java +++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/ClientGameTestContextImpl.java @@ -16,6 +16,11 @@ package net.fabricmc.fabric.impl.client.gametest; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; +import java.util.function.Predicate; + import com.google.common.base.Preconditions; import org.apache.commons.lang3.function.FailableConsumer; import org.apache.commons.lang3.function.FailableFunction; @@ -30,21 +35,77 @@ import net.minecraft.client.gui.widget.CyclingButtonWidget; import net.minecraft.client.gui.widget.PressableWidget; import net.minecraft.client.gui.widget.Widget; +import net.minecraft.client.option.GameOptions; +import net.minecraft.client.option.SimpleOption; +import net.minecraft.client.tutorial.TutorialStep; +import net.minecraft.client.util.ScreenshotRecorder; +import net.minecraft.sound.SoundCategory; import net.minecraft.text.Text; import net.minecraft.util.Nullables; import net.fabricmc.fabric.api.client.gametest.v1.ClientGameTestContext; import net.fabricmc.fabric.mixin.client.gametest.CyclingButtonWidgetAccessor; +import net.fabricmc.fabric.mixin.client.gametest.GameOptionsAccessor; import net.fabricmc.fabric.mixin.client.gametest.ScreenAccessor; +import net.fabricmc.loader.api.FabricLoader; public final class ClientGameTestContextImpl implements ClientGameTestContext { + private final ClientGameTestInputImpl input = new ClientGameTestInputImpl(this); + + private static final Map DEFAULT_GAME_OPTIONS = new HashMap<>(); + + public static void initGameOptions(GameOptions options) { + options.onboardAccessibility = false; + options.tutorialStep = TutorialStep.NONE; + options.getSoundVolumeOption(SoundCategory.MUSIC).setValue(0.0); + + ((GameOptionsAccessor) options).invokeAccept(new GameOptions.Visitor() { + @Override + public int visitInt(String key, int current) { + DEFAULT_GAME_OPTIONS.put(key, current); + return current; + } + + @Override + public boolean visitBoolean(String key, boolean current) { + DEFAULT_GAME_OPTIONS.put(key, current); + return current; + } + + @Override + public String visitString(String key, String current) { + DEFAULT_GAME_OPTIONS.put(key, current); + return current; + } + + @Override + public float visitFloat(String key, float current) { + DEFAULT_GAME_OPTIONS.put(key, current); + return current; + } + + @Override + public T visitObject(String key, T current, Function decoder, Function encoder) { + DEFAULT_GAME_OPTIONS.put(key, current); + return current; + } + + @Override + public void accept(String key, SimpleOption option) { + DEFAULT_GAME_OPTIONS.put(key, option.getValue()); + } + }); + } + @Override public void waitTick() { + ThreadingImpl.checkOnGametestThread("waitTick"); ThreadingImpl.runTick(); } @Override public void waitTicks(int ticks) { + ThreadingImpl.checkOnGametestThread("waitTicks"); Preconditions.checkArgument(ticks >= 0, "ticks cannot be negative"); for (int i = 0; i < ticks; i++) { @@ -52,13 +113,59 @@ public void waitTicks(int ticks) { } } + @Override + public void waitFor(Predicate predicate) { + ThreadingImpl.checkOnGametestThread("waitFor"); + Preconditions.checkNotNull(predicate, "predicate"); + waitFor(predicate, DEFAULT_TIMEOUT); + } + + @Override + public void waitFor(Predicate predicate, int timeout) { + ThreadingImpl.checkOnGametestThread("waitFor"); + Preconditions.checkNotNull(predicate, "predicate"); + + if (timeout == NO_TIMEOUT) { + while (!computeOnClient(predicate::test)) { + ThreadingImpl.runTick(); + } + } else { + Preconditions.checkArgument(timeout > 0, "timeout must be positive"); + + for (int i = 0; i < timeout; i++) { + if (computeOnClient(predicate::test)) { + return; + } + + ThreadingImpl.runTick(); + } + + if (!computeOnClient(predicate::test)) { + throw new AssertionError("Timed out waiting for predicate"); + } + } + } + + @Override + public void waitForScreen(@Nullable Class screenClass) { + ThreadingImpl.checkOnGametestThread("waitForScreen"); + + if (screenClass == null) { + waitFor(client -> client.currentScreen == null); + } else { + waitFor(client -> screenClass.isInstance(client.currentScreen)); + } + } + @Override public void setScreen(@Nullable Screen screen) { + ThreadingImpl.checkOnGametestThread("setScreen"); runOnClient(client -> client.setScreen(screen)); } @Override public void clickScreenButton(String translationKey) { + ThreadingImpl.checkOnGametestThread("clickScreenButton"); Preconditions.checkNotNull(translationKey, "translationKey"); runOnClient(client -> { @@ -73,6 +180,7 @@ public void clickScreenButton(String translationKey) { @Override public boolean tryClickScreenButton(String translationKey) { + ThreadingImpl.checkOnGametestThread("tryClickScreenButton"); Preconditions.checkNotNull(translationKey, "translationKey"); return computeOnClient(client -> tryClickScreenButtonImpl(client.currentScreen, translationKey)); @@ -120,8 +228,75 @@ private static boolean pressMatchingButton(ClickableWidget widget, String text) return false; } + @Override + public void takeScreenshot(String name) { + ThreadingImpl.checkOnGametestThread("takeScreenshot"); + Preconditions.checkNotNull(name, "name"); + takeScreenshot(name, 1); + } + + @Override + public void takeScreenshot(String name, int delay) { + ThreadingImpl.checkOnGametestThread("takeScreenshot"); + Preconditions.checkNotNull(name, "name"); + Preconditions.checkArgument(delay >= 0, "delay cannot be negative"); + + waitTicks(delay); + runOnClient(client -> { + ScreenshotRecorder.saveScreenshot(FabricLoader.getInstance().getGameDir().toFile(), name + ".png", client.getFramebuffer(), (message) -> { + }); + }); + } + + @Override + public ClientGameTestInputImpl getInput() { + return input; + } + + @Override + public void restoreDefaultGameOptions() { + ThreadingImpl.checkOnGametestThread("restoreDefaultGameOptions"); + + runOnClient(client -> { + ((GameOptionsAccessor) MinecraftClient.getInstance().options).invokeAccept(new GameOptions.Visitor() { + @Override + public int visitInt(String key, int current) { + return (Integer) DEFAULT_GAME_OPTIONS.get(key); + } + + @Override + public boolean visitBoolean(String key, boolean current) { + return (Boolean) DEFAULT_GAME_OPTIONS.get(key); + } + + @Override + public String visitString(String key, String current) { + return (String) DEFAULT_GAME_OPTIONS.get(key); + } + + @Override + public float visitFloat(String key, float current) { + return (Float) DEFAULT_GAME_OPTIONS.get(key); + } + + @SuppressWarnings("unchecked") + @Override + public T visitObject(String key, T current, Function decoder, Function encoder) { + return (T) DEFAULT_GAME_OPTIONS.get(key); + } + + @SuppressWarnings("unchecked") + @Override + public void accept(String key, SimpleOption option) { + option.setValue((T) DEFAULT_GAME_OPTIONS.get(key)); + } + }); + }); + } + @Override public void runOnClient(FailableConsumer action) throws E { + ThreadingImpl.checkOnGametestThread("runOnClient"); Preconditions.checkNotNull(action, "action"); ThreadingImpl.runOnClient(() -> action.accept(MinecraftClient.getInstance())); @@ -129,6 +304,7 @@ public void runOnClient(FailableConsumer T computeOnClient(FailableFunction function) throws E { + ThreadingImpl.checkOnGametestThread("computeOnClient"); Preconditions.checkNotNull(function, "function"); MutableObject result = new MutableObject<>(); diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/ClientGameTestInputImpl.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/ClientGameTestInputImpl.java new file mode 100644 index 0000000000..0590cd920f --- /dev/null +++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/ClientGameTestInputImpl.java @@ -0,0 +1,333 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.impl.client.gametest; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Function; + +import com.google.common.base.Preconditions; +import org.lwjgl.glfw.GLFW; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.option.GameOptions; +import net.minecraft.client.option.KeyBinding; +import net.minecraft.client.util.InputUtil; + +import net.fabricmc.fabric.api.client.gametest.v1.ClientGameTestContext; +import net.fabricmc.fabric.api.client.gametest.v1.ClientGameTestInput; +import net.fabricmc.fabric.mixin.client.gametest.KeyBindingAccessor; +import net.fabricmc.fabric.mixin.client.gametest.KeyboardAccessor; +import net.fabricmc.fabric.mixin.client.gametest.MouseAccessor; + +public final class ClientGameTestInputImpl implements ClientGameTestInput { + private static final Set KEYS_DOWN = new HashSet<>(); + private final ClientGameTestContext context; + + public ClientGameTestInputImpl(ClientGameTestContext context) { + this.context = context; + } + + public static boolean isKeyDown(int keyCode) { + return KEYS_DOWN.contains(InputUtil.Type.KEYSYM.createFromCode(keyCode)); + } + + public void clearKeysDown() { + for (InputUtil.Key key : new ArrayList<>(KEYS_DOWN)) { + releaseKey(key); + } + } + + @Override + public void pressKey(KeyBinding keyBinding) { + ThreadingImpl.checkOnGametestThread("pressKey"); + Preconditions.checkNotNull(keyBinding, "keyBinding"); + + pressKey(getBoundKey(keyBinding, "press")); + } + + @Override + public void pressKey(Function keyBindingGetter) { + ThreadingImpl.checkOnGametestThread("pressKey"); + Preconditions.checkNotNull(keyBindingGetter, "keyBindingGetter"); + + KeyBinding keyBinding = context.computeOnClient(client -> keyBindingGetter.apply(client.options)); + pressKey(keyBinding); + } + + @Override + public void pressKey(InputUtil.Key key) { + ThreadingImpl.checkOnGametestThread("pressKey"); + Preconditions.checkNotNull(key, "key"); + + if (KEYS_DOWN.add(key)) { + context.runOnClient(client -> pressOrReleaseKey(client, key, GLFW.GLFW_PRESS)); + } + } + + @Override + public void pressKey(int keyCode) { + ThreadingImpl.checkOnGametestThread("pressKey"); + + pressKey(InputUtil.Type.KEYSYM.createFromCode(keyCode)); + } + + @Override + public void pressMouse(int button) { + ThreadingImpl.checkOnGametestThread("pressMouse"); + + pressKey(InputUtil.Type.MOUSE.createFromCode(button)); + } + + @Override + public void pressControl() { + ThreadingImpl.checkOnGametestThread("pressControl"); + + pressKey(MinecraftClient.IS_SYSTEM_MAC ? InputUtil.GLFW_KEY_LEFT_SUPER : InputUtil.GLFW_KEY_LEFT_CONTROL); + } + + @Override + public void pressShift() { + ThreadingImpl.checkOnGametestThread("pressShift"); + + pressKey(InputUtil.GLFW_KEY_LEFT_SHIFT); + } + + @Override + public void pressAlt() { + ThreadingImpl.checkOnGametestThread("pressAlt"); + + pressKey(InputUtil.GLFW_KEY_LEFT_ALT); + } + + @Override + public void releaseKey(KeyBinding keyBinding) { + ThreadingImpl.checkOnGametestThread("releaseKey"); + Preconditions.checkNotNull(keyBinding, "keyBinding"); + + releaseKey(getBoundKey(keyBinding, "release")); + } + + @Override + public void releaseKey(Function keyBindingGetter) { + ThreadingImpl.checkOnGametestThread("releaseKey"); + Preconditions.checkNotNull(keyBindingGetter, "keyBindingGetter"); + + KeyBinding keyBinding = context.computeOnClient(client -> keyBindingGetter.apply(client.options)); + releaseKey(keyBinding); + } + + @Override + public void releaseKey(InputUtil.Key key) { + ThreadingImpl.checkOnGametestThread("releaseKey"); + Preconditions.checkNotNull(key, "key"); + + if (KEYS_DOWN.remove(key)) { + context.runOnClient(client -> pressOrReleaseKey(client, key, GLFW.GLFW_RELEASE)); + } + } + + @Override + public void releaseKey(int keyCode) { + ThreadingImpl.checkOnGametestThread("releaseKey"); + + releaseKey(InputUtil.Type.KEYSYM.createFromCode(keyCode)); + } + + @Override + public void releaseMouse(int button) { + ThreadingImpl.checkOnGametestThread("releaseMouse"); + + releaseKey(InputUtil.Type.MOUSE.createFromCode(button)); + } + + @Override + public void releaseControl() { + ThreadingImpl.checkOnGametestThread("releaseControl"); + + releaseKey(MinecraftClient.IS_SYSTEM_MAC ? InputUtil.GLFW_KEY_LEFT_SUPER : InputUtil.GLFW_KEY_LEFT_CONTROL); + } + + @Override + public void releaseShift() { + ThreadingImpl.checkOnGametestThread("releaseShift"); + + releaseKey(InputUtil.GLFW_KEY_LEFT_SHIFT); + } + + @Override + public void releaseAlt() { + ThreadingImpl.checkOnGametestThread("releaseAlt"); + + releaseKey(InputUtil.GLFW_KEY_LEFT_ALT); + } + + private static void pressOrReleaseKey(MinecraftClient client, InputUtil.Key key, int action) { + switch (key.getCategory()) { + case KEYSYM -> client.keyboard.onKey(client.getWindow().getHandle(), key.getCode(), 0, action, 0); + case SCANCODE -> client.keyboard.onKey(client.getWindow().getHandle(), GLFW.GLFW_KEY_UNKNOWN, key.getCode(), action, 0); + case MOUSE -> ((MouseAccessor) client.mouse).invokeOnMouseButton(client.getWindow().getHandle(), key.getCode(), action, 0); + } + } + + @Override + public void pressReleaseKey(KeyBinding keyBinding) { + ThreadingImpl.checkOnGametestThread("pressReleaseKey"); + Preconditions.checkNotNull(keyBinding, "keyBinding"); + + pressReleaseKey(getBoundKey(keyBinding, "press and release")); + } + + @Override + public void pressReleaseKey(Function keyBindingGetter) { + ThreadingImpl.checkOnGametestThread("pressReleaseKey"); + Preconditions.checkNotNull(keyBindingGetter, "keyBindingGetter"); + + KeyBinding keyBinding = context.computeOnClient(client -> keyBindingGetter.apply(client.options)); + pressReleaseKey(keyBinding); + } + + @Override + public void pressReleaseKey(InputUtil.Key key) { + ThreadingImpl.checkOnGametestThread("pressReleaseKey"); + Preconditions.checkNotNull(key, "key"); + + pressKey(key); + releaseKey(key); + } + + @Override + public void pressReleaseKey(int keyCode) { + ThreadingImpl.checkOnGametestThread("pressReleaseKey"); + + pressReleaseKey(InputUtil.Type.KEYSYM.createFromCode(keyCode)); + } + + @Override + public void pressReleaseMouse(int button) { + ThreadingImpl.checkOnGametestThread("pressReleaseMouse"); + + pressReleaseKey(InputUtil.Type.MOUSE.createFromCode(button)); + } + + @Override + public void holdKey(KeyBinding keyBinding, int ticks) { + ThreadingImpl.checkOnGametestThread("holdKey"); + Preconditions.checkNotNull(keyBinding, "keyBinding"); + Preconditions.checkArgument(ticks > 0, "ticks must be positive"); + + holdKey(getBoundKey(keyBinding, "hold"), ticks); + } + + @Override + public void holdKey(Function keyBindingGetter, int ticks) { + ThreadingImpl.checkOnGametestThread("holdKey"); + Preconditions.checkNotNull(keyBindingGetter, "keyBindingGetter"); + Preconditions.checkArgument(ticks > 0, "ticks must be positive"); + + KeyBinding keyBinding = context.computeOnClient(client -> keyBindingGetter.apply(client.options)); + holdKey(keyBinding, ticks); + } + + @Override + public void holdKey(InputUtil.Key key, int ticks) { + ThreadingImpl.checkOnGametestThread("holdKey"); + Preconditions.checkNotNull(key, "key"); + Preconditions.checkArgument(ticks > 0, "ticks must be positive"); + + pressKey(key); + context.waitTicks(ticks); + releaseKey(key); + } + + @Override + public void holdKey(int keyCode, int ticks) { + ThreadingImpl.checkOnGametestThread("holdKey"); + Preconditions.checkArgument(ticks > 0, "ticks must be positive"); + + holdKey(InputUtil.Type.KEYSYM.createFromCode(keyCode), ticks); + } + + @Override + public void holdMouse(int button, int ticks) { + ThreadingImpl.checkOnGametestThread("holdMouse"); + Preconditions.checkArgument(ticks > 0, "ticks must be positive"); + + holdKey(InputUtil.Type.MOUSE.createFromCode(button), ticks); + } + + @Override + public void typeChar(int codePoint) { + ThreadingImpl.checkOnGametestThread("typeChar"); + + context.runOnClient(client -> ((KeyboardAccessor) client.keyboard).invokeOnChar(client.getWindow().getHandle(), codePoint, 0)); + } + + @Override + public void typeChars(String chars) { + ThreadingImpl.checkOnGametestThread("typeChars"); + + context.runOnClient(client -> { + chars.chars().forEach(codePoint -> { + ((KeyboardAccessor) client.keyboard).invokeOnChar(client.getWindow().getHandle(), codePoint, 0); + }); + }); + } + + @Override + public void scroll(double amount) { + ThreadingImpl.checkOnGametestThread("scroll"); + + scroll(0, amount); + } + + @Override + public void scroll(double xAmount, double yAmount) { + ThreadingImpl.checkOnGametestThread("scroll"); + + context.runOnClient(client -> ((MouseAccessor) client.mouse).invokeOnMouseScroll(client.getWindow().getHandle(), xAmount, yAmount)); + } + + @Override + public void setCursorPos(double x, double y) { + ThreadingImpl.checkOnGametestThread("setCursorPos"); + + context.runOnClient(client -> ((MouseAccessor) client.mouse).invokeOnCursorPos(client.getWindow().getHandle(), x, y)); + } + + @Override + public void moveCursor(double deltaX, double deltaY) { + ThreadingImpl.checkOnGametestThread("moveCursor"); + + context.runOnClient(client -> { + double newX = client.mouse.getX() + deltaX; + double newY = client.mouse.getY() + deltaY; + ((MouseAccessor) client.mouse).invokeOnCursorPos(client.getWindow().getHandle(), newX, newY); + }); + } + + private static InputUtil.Key getBoundKey(KeyBinding keyBinding, String action) { + InputUtil.Key boundKey = ((KeyBindingAccessor) keyBinding).getBoundKey(); + + if (boundKey == InputUtil.UNKNOWN_KEY) { + throw new AssertionError("Cannot %s binding '%s' because it isn'keyBinding bound to a key".formatted(action, keyBinding.getTranslationKey())); + } + + return boundKey; + } +} diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/FabricClientGameTestRunner.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/FabricClientGameTestRunner.java index bf16be7f97..433d762a21 100644 --- a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/FabricClientGameTestRunner.java +++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/FabricClientGameTestRunner.java @@ -21,9 +21,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import net.minecraft.client.MinecraftClient; import net.minecraft.client.gui.screen.TitleScreen; -import net.fabricmc.fabric.api.client.gametest.v1.ClientGameTestContext; import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest; import net.fabricmc.loader.api.FabricLoader; @@ -32,18 +32,26 @@ public class FabricClientGameTestRunner { private static final String ENTRYPOINT_KEY = "fabric-client-gametest"; public static void start() { + // make the game think the window is focused + MinecraftClient.getInstance().onWindowFocusChanged(true); + List gameTests = FabricLoader.getInstance().getEntrypoints(ENTRYPOINT_KEY, FabricClientGameTest.class); + ThreadingImpl.runTestThread(() -> { - ClientGameTestContext context = new ClientGameTestContextImpl(); + ClientGameTestContextImpl context = new ClientGameTestContextImpl(); boolean failed = false; for (FabricClientGameTest gameTest : gameTests) { + context.restoreDefaultGameOptions(); + try { gameTest.runTest(context); } catch (Throwable e) { LOGGER.error("Failed test {}", gameTest.getClass().getName(), e); failed = true; } finally { + context.getInput().clearKeysDown(); + // Open the title screen to reset the state for the next gametest. // If the gametest API was used correctly, we should be in the menus somewhere because any test // world should have been closed at the end of a try-with-resources statement. diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/FabricClientTestHelper.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/FabricClientTestHelper.java deleted file mode 100644 index dcb7d08e1f..0000000000 --- a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/FabricClientTestHelper.java +++ /dev/null @@ -1,226 +0,0 @@ -/* - * Copyright (c) 2016, 2017, 2018, 2019 FabricMC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package net.fabricmc.fabric.impl.client.gametest; - -import java.util.Objects; -import java.util.function.Function; -import java.util.function.Predicate; - -import org.apache.commons.lang3.function.FailableConsumer; -import org.apache.commons.lang3.function.FailableFunction; -import org.apache.commons.lang3.mutable.MutableObject; - -import net.minecraft.SharedConstants; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.gui.Drawable; -import net.minecraft.client.gui.screen.GameMenuScreen; -import net.minecraft.client.gui.screen.Screen; -import net.minecraft.client.gui.screen.TitleScreen; -import net.minecraft.client.gui.screen.ingame.CreativeInventoryScreen; -import net.minecraft.client.gui.screen.ingame.InventoryScreen; -import net.minecraft.client.gui.screen.multiplayer.ConnectScreen; -import net.minecraft.client.gui.screen.world.LevelLoadingScreen; -import net.minecraft.client.gui.widget.ButtonWidget; -import net.minecraft.client.gui.widget.ClickableWidget; -import net.minecraft.client.gui.widget.CyclingButtonWidget; -import net.minecraft.client.gui.widget.PressableWidget; -import net.minecraft.client.gui.widget.Widget; -import net.minecraft.client.network.ServerAddress; -import net.minecraft.client.network.ServerInfo; -import net.minecraft.client.option.Perspective; -import net.minecraft.client.util.ScreenshotRecorder; -import net.minecraft.text.Text; - -import net.fabricmc.fabric.mixin.client.gametest.CyclingButtonWidgetAccessor; -import net.fabricmc.fabric.mixin.client.gametest.ScreenAccessor; -import net.fabricmc.fabric.mixin.client.gametest.TitleScreenAccessor; -import net.fabricmc.loader.api.FabricLoader; - -public final class FabricClientTestHelper { - public static void waitForLoadingComplete() { - // client is not ticking and can't accept tasks, waitFor doesn't work so we'll do this until then - while (!ThreadingImpl.clientCanAcceptTasks) { - runTick(); - - try { - //noinspection BusyWait - Thread.sleep(50); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - } - - waitFor("Loading to complete", client -> client.getOverlay() == null, 5 * SharedConstants.TICKS_PER_MINUTE); - } - - public static void waitForScreen(Class screenClass) { - waitFor("Screen %s".formatted(screenClass.getName()), client -> client.currentScreen != null && client.currentScreen.getClass() == screenClass); - } - - public static void openGameMenu() { - setScreen((client) -> new GameMenuScreen(true)); - waitForScreen(GameMenuScreen.class); - } - - public static void openInventory() { - setScreen((client) -> new InventoryScreen(Objects.requireNonNull(client.player))); - - boolean creative = computeOnClient(client -> Objects.requireNonNull(client.player).isCreative()); - waitForScreen(creative ? CreativeInventoryScreen.class : InventoryScreen.class); - } - - public static void closeScreen() { - setScreen((client) -> null); - } - - private static void setScreen(Function screenSupplier) { - runOnClient(client -> client.setScreen(screenSupplier.apply(client))); - } - - public static void takeScreenshot(String name) { - takeScreenshot(name, 1); - } - - public static void takeScreenshot(String name, int delayTicks) { - // Allow time for any screens to open - runTicks(delayTicks); - - runOnClient(client -> { - ScreenshotRecorder.saveScreenshot(FabricLoader.getInstance().getGameDir().toFile(), name + ".png", client.getFramebuffer(), (message) -> { - }); - }); - } - - public static void clickScreenButton(String translationKey) { - final String buttonText = Text.translatable(translationKey).getString(); - - waitFor("Click button" + buttonText, client -> { - final Screen screen = client.currentScreen; - - if (screen == null) { - return false; - } - - final ScreenAccessor screenAccessor = (ScreenAccessor) screen; - - for (Drawable drawable : screenAccessor.getDrawables()) { - if (drawable instanceof PressableWidget pressableWidget && pressMatchingButton(pressableWidget, buttonText)) { - return true; - } - - if (drawable instanceof Widget widget) { - widget.forEachChild(clickableWidget -> pressMatchingButton(clickableWidget, buttonText)); - } - } - - // Was unable to find the button to press - return false; - }); - } - - private static boolean pressMatchingButton(ClickableWidget widget, String text) { - if (widget instanceof ButtonWidget buttonWidget) { - if (text.equals(buttonWidget.getMessage().getString())) { - buttonWidget.onPress(); - return true; - } - } - - if (widget instanceof CyclingButtonWidget buttonWidget) { - CyclingButtonWidgetAccessor accessor = (CyclingButtonWidgetAccessor) buttonWidget; - - if (text.equals(accessor.getOptionText().getString())) { - buttonWidget.onPress(); - return true; - } - } - - return false; - } - - public static void waitForWorldTicks(long ticks) { - // Wait for the world to be loaded and get the start ticks - waitFor("World load", client -> client.world != null && !(client.currentScreen instanceof LevelLoadingScreen), 30 * SharedConstants.TICKS_PER_MINUTE); - final long startTicks = computeOnClient(client -> client.world.getTime()); - waitFor("World load", client -> Objects.requireNonNull(client.world).getTime() > startTicks + ticks, 10 * SharedConstants.TICKS_PER_MINUTE); - } - - public static void enableDebugHud() { - runOnClient(client -> client.inGameHud.getDebugHud().toggleDebugHud()); - } - - public static void setPerspective(Perspective perspective) { - runOnClient(client -> client.options.setPerspective(perspective)); - } - - public static void connectToServer(TestDedicatedServer server) { - runOnClient(client -> { - final var serverInfo = new ServerInfo("localhost", server.getConnectionAddress(), ServerInfo.ServerType.OTHER); - ConnectScreen.connect(client.currentScreen, client, ServerAddress.parse(server.getConnectionAddress()), serverInfo, false, null); - }); - } - - public static void waitForTitleScreenFade() { - waitFor("Title screen fade", client -> { - if (!(client.currentScreen instanceof TitleScreen titleScreen)) { - return false; - } - - return !((TitleScreenAccessor) titleScreen).getDoBackgroundFade(); - }); - } - - public static void waitForServerStop() { - waitFor("Server stop", client -> !ThreadingImpl.isServerRunning, SharedConstants.TICKS_PER_MINUTE); - } - - private static void waitFor(String what, Predicate predicate) { - waitFor(what, predicate, 10 * SharedConstants.TICKS_PER_SECOND); - } - - private static void waitFor(String what, Predicate predicate, int timeoutTicks) { - int tickCount; - - for (tickCount = 0; tickCount < timeoutTicks && !computeOnClient(predicate::test); tickCount++) { - runTick(); - } - - if (tickCount == timeoutTicks && !computeOnClient(predicate::test)) { - throw new RuntimeException("Timed out waiting for " + what); - } - } - - public static void runTicks(int ticks) { - for (int i = 0; i < ticks; i++) { - runTick(); - } - } - - public static void runTick() { - ThreadingImpl.runTick(); - } - - public static void runOnClient(FailableConsumer action) throws E { - ThreadingImpl.runOnClient(() -> action.accept(MinecraftClient.getInstance())); - } - - public static T computeOnClient(FailableFunction action) throws E { - MutableObject result = new MutableObject<>(); - runOnClient(client -> result.setValue(action.apply(client))); - return result.getValue(); - } -} diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/ThreadingImpl.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/ThreadingImpl.java index a739cbc0dc..a0a0a1719a 100644 --- a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/ThreadingImpl.java +++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/ThreadingImpl.java @@ -23,6 +23,8 @@ import org.apache.commons.lang3.function.FailableRunnable; import org.apache.commons.lang3.mutable.MutableObject; import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** *

Implementation notes

@@ -64,6 +66,8 @@ public final class ThreadingImpl { private ThreadingImpl() { } + private static final Logger LOGGER = LoggerFactory.getLogger("fabric-client-gametest-api-v1"); + public static final int PHASE_TICK = 0; public static final int PHASE_SERVER_TASKS = 1; public static final int PHASE_CLIENT_TASKS = 2; @@ -105,7 +109,7 @@ public static void runTestThread(Runnable test) { try { test.run(); } catch (Throwable e) { - e.printStackTrace(); + LOGGER.error("Failed to run client gametests", e); System.exit(1); } finally { PHASER.arriveAndDeregister(); @@ -125,10 +129,14 @@ public static void runTestThread(Runnable test) { testThread.start(); } + public static void checkOnGametestThread(String methodName) { + Preconditions.checkState(Thread.currentThread() == testThread, "%s can only be called from the client gametest thread", methodName); + } + @SuppressWarnings("unchecked") public static void runOnClient(FailableRunnable action) throws E { Preconditions.checkNotNull(action, "action"); - Preconditions.checkState(Thread.currentThread() == testThread, "runOnClient can only be called from the test thread"); + checkOnGametestThread("runOnClient"); Preconditions.checkState(clientCanAcceptTasks, "runOnClient called when no client is running"); MutableObject thrown = new MutableObject<>(); @@ -159,7 +167,7 @@ public static void runOnClient(FailableRunnable action) @SuppressWarnings("unchecked") public static void runOnServer(FailableRunnable action) throws E { Preconditions.checkNotNull(action, "action"); - Preconditions.checkState(Thread.currentThread() == testThread, "runOnServer can only be called from the test thread"); + checkOnGametestThread("runOnServer"); Preconditions.checkState(serverCanAcceptTasks, "runOnServer called when no server is running"); MutableObject thrown = new MutableObject<>(); @@ -188,7 +196,7 @@ public static void runOnServer(FailableRunnable action) } public static void runTick() { - Preconditions.checkState(Thread.currentThread() == testThread, "runTick can only be called from the test thread"); + checkOnGametestThread("runTick"); if (clientCanAcceptTasks) { CLIENT_SEMAPHORE.release(); diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/GameOptionsAccessor.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/GameOptionsAccessor.java new file mode 100644 index 0000000000..a4cf689aa0 --- /dev/null +++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/GameOptionsAccessor.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.mixin.client.gametest; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +import net.minecraft.client.option.GameOptions; + +@Mixin(GameOptions.class) +public interface GameOptionsAccessor { + @Invoker + void invokeAccept(GameOptions.Visitor visitor); +} diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/GameOptionsMixin.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/GameOptionsMixin.java new file mode 100644 index 0000000000..eb36fb0451 --- /dev/null +++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/GameOptionsMixin.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.mixin.client.gametest; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import net.minecraft.client.option.GameOptions; + +import net.fabricmc.fabric.impl.client.gametest.ClientGameTestContextImpl; + +@Mixin(GameOptions.class) +public class GameOptionsMixin { + @Inject(method = "", at = @At("RETURN")) + private void onCreateGameOptions(CallbackInfo ci) { + ClientGameTestContextImpl.initGameOptions((GameOptions) (Object) this); + } +} diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/InputUtilMixin.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/InputUtilMixin.java index 737be7af43..0351fa1b75 100644 --- a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/InputUtilMixin.java +++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/InputUtilMixin.java @@ -24,11 +24,13 @@ import net.minecraft.client.util.InputUtil; +import net.fabricmc.fabric.impl.client.gametest.ClientGameTestInputImpl; + @Mixin(InputUtil.class) public class InputUtilMixin { @Inject(method = "isKeyPressed", at = @At("HEAD"), cancellable = true) - private static void keysNeverPressed(CallbackInfoReturnable cir) { - cir.setReturnValue(false); + private static void useGameTestInputForKeyPressed(long window, int keyCode, CallbackInfoReturnable cir) { + cir.setReturnValue(ClientGameTestInputImpl.isKeyDown(keyCode)); } @Inject(method = {"setKeyboardCallbacks", "setMouseCallbacks"}, at = @At("HEAD"), cancellable = true) diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/KeyBindingAccessor.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/KeyBindingAccessor.java new file mode 100644 index 0000000000..d3e240bb9f --- /dev/null +++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/KeyBindingAccessor.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.mixin.client.gametest; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +import net.minecraft.client.option.KeyBinding; +import net.minecraft.client.util.InputUtil; + +@Mixin(KeyBinding.class) +public interface KeyBindingAccessor { + @Accessor + InputUtil.Key getBoundKey(); +} diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/KeyboardAccessor.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/KeyboardAccessor.java new file mode 100644 index 0000000000..b6f573b81e --- /dev/null +++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/KeyboardAccessor.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.mixin.client.gametest; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +import net.minecraft.client.Keyboard; + +@Mixin(Keyboard.class) +public interface KeyboardAccessor { + @Invoker + void invokeOnChar(long window, int codePoint, int modifiers); +} diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/MinecraftClientMixin.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/MinecraftClientMixin.java index cf1d7ffceb..9c57131be8 100644 --- a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/MinecraftClientMixin.java +++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/MinecraftClientMixin.java @@ -147,7 +147,9 @@ private void onDisconnectBusyWait(CallbackInfo ci) { @Inject(method = "getInstance", at = @At("HEAD")) private static void checkThreadOnGetInstance(CallbackInfoReturnable cir) { - // TODO: add suggestion of runOnClient etc when API methods are added - Preconditions.checkState(Thread.currentThread() != ThreadingImpl.testThread, "MinecraftClient.getInstance() cannot be called from the test thread"); + Preconditions.checkState( + Thread.currentThread() != ThreadingImpl.testThread, + "MinecraftClient.getInstance() cannot be called from the gametest thread. Try using ClientGameTestContext.runOnClient or ClientGameTestContext.computeOnClient" + ); } } diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/MouseAccessor.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/MouseAccessor.java new file mode 100644 index 0000000000..74af099958 --- /dev/null +++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/MouseAccessor.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.mixin.client.gametest; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +import net.minecraft.client.Mouse; + +@Mixin(Mouse.class) +public interface MouseAccessor { + @Invoker + void invokeOnMouseButton(long window, int button, int action, int mods); + + @Invoker + void invokeOnMouseScroll(long window, double horizontal, double vertical); + + @Invoker + void invokeOnCursorPos(long window, double x, double y); +} diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/WindowMixin.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/WindowMixin.java new file mode 100644 index 0000000000..cce6d8e78e --- /dev/null +++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/WindowMixin.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.fabricmc.fabric.mixin.client.gametest; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import net.minecraft.client.util.Window; + +@Mixin(Window.class) +public class WindowMixin { + @Inject(method = {"onWindowFocusChanged", "onCursorEnterChanged"}, at = @At("HEAD"), cancellable = true) + private void cancelEvents(CallbackInfo ci) { + ci.cancel(); + } +} diff --git a/fabric-client-gametest-api-v1/src/client/resources/fabric-client-gametest-api-v1.accesswidener b/fabric-client-gametest-api-v1/src/client/resources/fabric-client-gametest-api-v1.accesswidener new file mode 100644 index 0000000000..622ee90f61 --- /dev/null +++ b/fabric-client-gametest-api-v1/src/client/resources/fabric-client-gametest-api-v1.accesswidener @@ -0,0 +1,2 @@ +accessWidener v2 named +accessible class net/minecraft/client/option/GameOptions$Visitor diff --git a/fabric-client-gametest-api-v1/src/client/resources/fabric-client-gametest-api-v1.mixins.json b/fabric-client-gametest-api-v1/src/client/resources/fabric-client-gametest-api-v1.mixins.json index 3b10f16fa1..6afc114a99 100644 --- a/fabric-client-gametest-api-v1/src/client/resources/fabric-client-gametest-api-v1.mixins.json +++ b/fabric-client-gametest-api-v1/src/client/resources/fabric-client-gametest-api-v1.mixins.json @@ -12,9 +12,15 @@ }, "client": [ "CyclingButtonWidgetAccessor", + "GameOptionsAccessor", + "GameOptionsMixin", "InputUtilMixin", + "KeyBindingAccessor", + "KeyboardAccessor", "MinecraftClientMixin", + "MouseAccessor", "ScreenAccessor", - "TitleScreenAccessor" + "TitleScreenAccessor", + "WindowMixin" ] } diff --git a/fabric-client-gametest-api-v1/src/client/resources/fabric.mod.json b/fabric-client-gametest-api-v1/src/client/resources/fabric.mod.json index 66b17600a4..4de3a7388f 100644 --- a/fabric-client-gametest-api-v1/src/client/resources/fabric.mod.json +++ b/fabric-client-gametest-api-v1/src/client/resources/fabric.mod.json @@ -23,6 +23,7 @@ "mixins": [ "fabric-client-gametest-api-v1.mixins.json" ], + "accessWidener": "fabric-client-gametest-api-v1.accesswidener", "custom": { "fabric-api:module-lifecycle": "experimental" } diff --git a/fabric-client-gametest-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/gametest/client/ClientGameTestTest.java b/fabric-client-gametest-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/gametest/client/ClientGameTestTest.java index 361aa3ac4b..4460826cef 100644 --- a/fabric-client-gametest-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/gametest/client/ClientGameTestTest.java +++ b/fabric-client-gametest-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/gametest/client/ClientGameTestTest.java @@ -16,148 +16,169 @@ package net.fabricmc.fabric.test.gametest.client; -import static net.fabricmc.fabric.impl.client.gametest.FabricClientTestHelper.clickScreenButton; -import static net.fabricmc.fabric.impl.client.gametest.FabricClientTestHelper.closeScreen; -import static net.fabricmc.fabric.impl.client.gametest.FabricClientTestHelper.computeOnClient; -import static net.fabricmc.fabric.impl.client.gametest.FabricClientTestHelper.connectToServer; -import static net.fabricmc.fabric.impl.client.gametest.FabricClientTestHelper.enableDebugHud; -import static net.fabricmc.fabric.impl.client.gametest.FabricClientTestHelper.openGameMenu; -import static net.fabricmc.fabric.impl.client.gametest.FabricClientTestHelper.openInventory; -import static net.fabricmc.fabric.impl.client.gametest.FabricClientTestHelper.setPerspective; -import static net.fabricmc.fabric.impl.client.gametest.FabricClientTestHelper.takeScreenshot; -import static net.fabricmc.fabric.impl.client.gametest.FabricClientTestHelper.waitForScreen; -import static net.fabricmc.fabric.impl.client.gametest.FabricClientTestHelper.waitForServerStop; -import static net.fabricmc.fabric.impl.client.gametest.FabricClientTestHelper.waitForTitleScreenFade; -import static net.fabricmc.fabric.impl.client.gametest.FabricClientTestHelper.waitForWorldTicks; - import java.io.IOException; import java.io.UncheckedIOException; import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Objects; import com.mojang.authlib.GameProfile; import org.spongepowered.asm.mixin.MixinEnvironment; +import net.minecraft.SharedConstants; import net.minecraft.client.MinecraftClient; -import net.minecraft.client.gui.screen.AccessibilityOnboardingScreen; import net.minecraft.client.gui.screen.ConfirmScreen; +import net.minecraft.client.gui.screen.GameMenuScreen; import net.minecraft.client.gui.screen.ReconfiguringScreen; import net.minecraft.client.gui.screen.TitleScreen; +import net.minecraft.client.gui.screen.multiplayer.ConnectScreen; import net.minecraft.client.gui.screen.multiplayer.MultiplayerScreen; import net.minecraft.client.gui.screen.world.CreateWorldScreen; +import net.minecraft.client.gui.screen.world.LevelLoadingScreen; import net.minecraft.client.gui.screen.world.SelectWorldScreen; +import net.minecraft.client.network.ServerAddress; +import net.minecraft.client.network.ServerInfo; import net.minecraft.client.option.Perspective; +import net.minecraft.client.util.InputUtil; import net.fabricmc.fabric.api.client.gametest.v1.ClientGameTestContext; import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest; import net.fabricmc.fabric.impl.client.gametest.TestDedicatedServer; +import net.fabricmc.fabric.impl.client.gametest.ThreadingImpl; import net.fabricmc.loader.api.FabricLoader; public class ClientGameTestTest implements FabricClientGameTest { public void runTest(ClientGameTestContext context) { - final boolean onboardAccessibility = computeOnClient(client -> client.options.onboardAccessibility); - - if (onboardAccessibility) { - waitForScreen(AccessibilityOnboardingScreen.class); - takeScreenshot("onboarding_screen"); - clickScreenButton("gui.continue"); - } - { - waitForScreen(TitleScreen.class); - waitForTitleScreenFade(); - takeScreenshot("title_screen", 0); - clickScreenButton("menu.singleplayer"); + context.takeScreenshot("title_screen", 0); + context.clickScreenButton("menu.singleplayer"); } if (!isDirEmpty(FabricLoader.getInstance().getGameDir().resolve("saves"))) { - waitForScreen(SelectWorldScreen.class); - takeScreenshot("select_world_screen"); - clickScreenButton("selectWorld.create"); + context.waitForScreen(SelectWorldScreen.class); + context.takeScreenshot("select_world_screen"); + context.clickScreenButton("selectWorld.create"); } { - waitForScreen(CreateWorldScreen.class); - clickScreenButton("selectWorld.gameMode"); - clickScreenButton("selectWorld.gameMode"); - takeScreenshot("create_world_screen"); - clickScreenButton("selectWorld.create"); + context.waitForScreen(CreateWorldScreen.class); + context.clickScreenButton("selectWorld.gameMode"); + context.clickScreenButton("selectWorld.gameMode"); + context.takeScreenshot("create_world_screen"); + context.clickScreenButton("selectWorld.create"); } { // API test mods use experimental features - waitForScreen(ConfirmScreen.class); - clickScreenButton("gui.yes"); + context.waitForScreen(ConfirmScreen.class); + context.clickScreenButton("gui.yes"); } { - enableDebugHud(); - waitForWorldTicks(200); - takeScreenshot("in_game_overworld", 0); + enableDebugHud(context); + waitForWorldTicks(context, 200); + context.takeScreenshot("in_game_overworld", 0); + } + + { + context.getInput().pressReleaseKey(options -> options.chatKey); + context.waitTick(); + context.getInput().typeChars("Hello, World!"); + context.getInput().pressReleaseKey(InputUtil.GLFW_KEY_ENTER); + context.takeScreenshot("chat_message_sent", 5); } MixinEnvironment.getCurrentEnvironment().audit(); { // See if the player render events are working. - setPerspective(Perspective.THIRD_PERSON_BACK); - takeScreenshot("in_game_overworld_third_person"); - setPerspective(Perspective.FIRST_PERSON); + setPerspective(context, Perspective.THIRD_PERSON_BACK); + context.takeScreenshot("in_game_overworld_third_person"); + setPerspective(context, Perspective.FIRST_PERSON); } { - openInventory(); - takeScreenshot("in_game_inventory"); - closeScreen(); + context.getInput().pressReleaseKey(options -> options.inventoryKey); + context.takeScreenshot("in_game_inventory"); + context.setScreen(null); } { - openGameMenu(); - takeScreenshot("game_menu"); - clickScreenButton("menu.returnToMenu"); - waitForScreen(TitleScreen.class); - waitForServerStop(); + context.setScreen(new GameMenuScreen(true)); + context.takeScreenshot("game_menu"); + context.clickScreenButton("menu.returnToMenu"); + context.waitForScreen(TitleScreen.class); + waitForServerStop(context); } try (var server = new TestDedicatedServer()) { - connectToServer(server); - waitForWorldTicks(5); + connectToServer(context, server); + waitForWorldTicks(context, 5); - final GameProfile profile = computeOnClient(MinecraftClient::getGameProfile); + final GameProfile profile = context.computeOnClient(MinecraftClient::getGameProfile); server.runCommand("op " + profile.getName()); server.runCommand("gamemode creative " + profile.getName()); - waitForWorldTicks(20); - takeScreenshot("server_in_game", 0); + waitForWorldTicks(context, 20); + context.takeScreenshot("server_in_game", 0); { // Test that we can enter and exit configuration server.runCommand("debugconfig config " + profile.getName()); - waitForScreen(ReconfiguringScreen.class); - takeScreenshot("server_config"); + context.waitForScreen(ReconfiguringScreen.class); + context.takeScreenshot("server_config"); server.runCommand("debugconfig unconfig " + profile.getId()); - waitForWorldTicks(1); + waitForWorldTicks(context, 1); } - openGameMenu(); - takeScreenshot("server_game_menu"); - clickScreenButton("menu.disconnect"); + context.setScreen(new GameMenuScreen(true)); + context.takeScreenshot("server_game_menu"); + context.clickScreenButton("menu.disconnect"); - waitForScreen(MultiplayerScreen.class); - clickScreenButton("gui.back"); + context.waitForScreen(MultiplayerScreen.class); + context.clickScreenButton("gui.back"); } { - waitForScreen(TitleScreen.class); - clickScreenButton("menu.quit"); + context.waitForScreen(TitleScreen.class); + context.clickScreenButton("menu.quit"); } } - private boolean isDirEmpty(Path path) { + private static boolean isDirEmpty(Path path) { try (DirectoryStream directory = Files.newDirectoryStream(path)) { return !directory.iterator().hasNext(); } catch (IOException e) { throw new UncheckedIOException(e); } } + + private static void enableDebugHud(ClientGameTestContext context) { + context.runOnClient(client -> client.inGameHud.getDebugHud().toggleDebugHud()); + } + + private static void setPerspective(ClientGameTestContext context, Perspective perspective) { + context.runOnClient(client -> client.options.setPerspective(perspective)); + } + + // TODO: replace with world builder + private static void waitForWorldTicks(ClientGameTestContext context, long ticks) { + // Wait for the world to be loaded and get the start ticks + context.waitFor(client -> client.world != null && !(client.currentScreen instanceof LevelLoadingScreen), 30 * SharedConstants.TICKS_PER_MINUTE); + final long startTicks = context.computeOnClient(client -> client.world.getTime()); + context.waitFor(client -> Objects.requireNonNull(client.world).getTime() > startTicks + ticks, 10 * SharedConstants.TICKS_PER_MINUTE); + } + + // TODO: replace with function on TestDedicatedServer + private static void connectToServer(ClientGameTestContext context, TestDedicatedServer server) { + context.runOnClient(client -> { + final var serverInfo = new ServerInfo("localhost", server.getConnectionAddress(), ServerInfo.ServerType.OTHER); + ConnectScreen.connect(client.currentScreen, client, ServerAddress.parse(server.getConnectionAddress()), serverInfo, false, null); + }); + } + + // TODO: move into close methods of TestDedicatedServer and TestWorld + private static void waitForServerStop(ClientGameTestContext context) { + context.waitFor(client -> !ThreadingImpl.isServerRunning, SharedConstants.TICKS_PER_MINUTE); + } } From 6c6eb17bd8908775346c384fe87c875ebe7fd833 Mon Sep 17 00:00:00 2001 From: joe Date: Wed, 11 Dec 2024 15:53:09 +0000 Subject: [PATCH 07/15] Rename and document input methods --- .../gametest/v1/ClientGameTestInput.java | 279 ++++++++++++++++-- .../gametest/ClientGameTestInputImpl.java | 106 +++---- .../gametest/client/ClientGameTestTest.java | 6 +- 3 files changed, 303 insertions(+), 88 deletions(-) diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/ClientGameTestInput.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/ClientGameTestInput.java index b83d099280..90753d1dd1 100644 --- a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/ClientGameTestInput.java +++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/ClientGameTestInput.java @@ -18,6 +18,7 @@ import java.util.function.Function; +import net.minecraft.client.gui.screen.Screen; import net.minecraft.client.option.GameOptions; import net.minecraft.client.option.KeyBinding; import net.minecraft.client.util.InputUtil; @@ -26,68 +27,282 @@ * The client gametest input handler used to simulate inputs to the client. */ public interface ClientGameTestInput { - // TODO: document all these methods - void pressKey(KeyBinding keyBinding); - - void pressKey(Function keyBindingGetter); - - void pressKey(InputUtil.Key key); - - void pressKey(int keyCode); - - void pressMouse(int button); - - void pressControl(); - - void pressShift(); - - void pressAlt(); - + /** + * Starts holding down a key binding. The key binding will be held until it is released. The key binding must be + * bound. + * + * @param keyBinding The key binding to hold + * @see #releaseKey(KeyBinding) + * @see #pressKey(KeyBinding) + * @see #holdKey(Function) + */ + void holdKey(KeyBinding keyBinding); + + /** + * Starts holding down a key binding. The key binding will be held until it is released. The key binding must be + * bound. + * + * @param keyBindingGetter The function to get the key binding from the game options + * @see #releaseKey(Function) + * @see #pressKey(Function) + * @see #holdKey(KeyBinding) + */ + void holdKey(Function keyBindingGetter); + + /** + * Starts holding down a key or mouse button. The key will be held until it is released. + * + * @param key The key or mouse button to hold + * @see #releaseKey(InputUtil.Key) + * @see #pressKey(InputUtil.Key) + */ + void holdKey(InputUtil.Key key); + + /** + * Starts holding down a key. The key will be held until it is released. + * + * @param keyCode The key code of the key to hold + * @see #releaseKey(int) + * @see #pressKey(int) + */ + void holdKey(int keyCode); + + /** + * Starts holding down a mouse button. The mouse button will be held until it is released. + * + * @param button The mouse button to hold + * @see #releaseMouse(int) + * @see #pressMouse(int) + */ + void holdMouse(int button); + + /** + * Starts holding down left control, or left super on macOS. Suitable for triggering + * {@link Screen#hasControlDown()}. The key will be held until it is released. + * + * @see #releaseControl() + */ + void holdControl(); + + /** + * Starts holding down left shift. Suitable for triggering {@link Screen#hasShiftDown()}. The key will be held until + * it is released. + * + * @see #releaseShift() + */ + void holdShift(); + + /** + * Starts holding down left alt. Suitable for triggering {@link Screen#hasAltDown()}. The key will be held until it + * is released. + * + * @see #releaseAlt() + */ + void holdAlt(); + + /** + * Releases a key binding. The key binding must be bound. + * + * @param keyBinding The key binding to release + * @see #holdKey(KeyBinding) + * @see #releaseKey(Function) + */ void releaseKey(KeyBinding keyBinding); + /** + * Releases a key binding. The key binding must be bound. + * + * @param keyBindingGetter The function to get the key binding from the game options + * @see #holdKey(Function) + * @see #releaseKey(KeyBinding) + */ void releaseKey(Function keyBindingGetter); + /** + * Releases a key or mouse button. + * + * @param key The key or mouse button to release + * @see #holdKey(InputUtil.Key) + */ void releaseKey(InputUtil.Key key); + /** + * Releases a key. + * + * @param keyCode The key code of the key to release + * @see #holdKey(int) + */ void releaseKey(int keyCode); + /** + * Releases a mouse button. + * + * @param button The mouse button to release + * @see #holdMouse(int) + */ void releaseMouse(int button); + /** + * Releases left control, or left super on macOS. Suitable for un-triggering {@link Screen#hasControlDown()}. + * + * @see #holdControl() + */ void releaseControl(); + /** + * Releases left shift. Suitable for un-triggering {@link Screen#hasShiftDown()}. + * + * @see #holdShift() + */ void releaseShift(); + /** + * Releases left alt. Suitable for un-triggering {@link Screen#hasAltDown()}. + * + * @see #holdAlt() + */ void releaseAlt(); - void pressReleaseKey(KeyBinding keyBinding); - - void pressReleaseKey(Function keyBindingGetter); - - void pressReleaseKey(InputUtil.Key key); - - void pressReleaseKey(int keyCode); - - void pressReleaseMouse(int button); - - void holdKey(KeyBinding keyBinding, int ticks); + /** + * Presses and releases a key binding. The key binding must be bound. + * + * @param keyBinding The key binding to press + * @see #holdKey(KeyBinding) + * @see #pressKey(Function) + */ + void pressKey(KeyBinding keyBinding); - void holdKey(Function keyBindingGetter, int ticks); + /** + * Presses and releases a key binding. The key binding must be bound. + * + * @param keyBindingGetter The function to get the key binding from the game options + * @see #holdKey(Function) + * @see #pressKey(KeyBinding) + */ + void pressKey(Function keyBindingGetter); - void holdKey(InputUtil.Key key, int ticks); + /** + * Presses and releases a key or mouse button. + * + * @param key The key or mouse button to press. + * @see #holdKey(InputUtil.Key) + */ + void pressKey(InputUtil.Key key); - void holdKey(int keyCode, int ticks); + /** + * Presses and releases a key. + * + * @param keyCode The key code of the key to press + * @see #holdKey(int) + */ + void pressKey(int keyCode); - void holdMouse(int button, int ticks); + /** + * Presses and releases a mouse button. + * + * @param button The mouse button to press + * @see #holdMouse(int) + */ + void pressMouse(int button); + /** + * Holds a key binding for the specified number of ticks and then releases it. Waits until this process is finished. + * The key binding must be bound. + * + * @param keyBinding The key binding to hold + * @param ticks The number of ticks to hold the key binding for + * @see #holdKey(KeyBinding) + * @see #holdKeyFor(Function, int) + */ + void holdKeyFor(KeyBinding keyBinding, int ticks); + + /** + * Holds a key binding for the specified number of ticks and then releases it. Waits until this process is finished. + * The key binding must be bound. + * + * @param keyBindingGetter The key binding to hold + * @param ticks The number of ticks to hold the key binding for + * @see #holdKey(Function) + * @see #holdKeyFor(Function, int) + */ + void holdKeyFor(Function keyBindingGetter, int ticks); + + /** + * Holds a key or mouse button for the specified number of ticks and then releases it. Waits until this process is + * finished. + * + * @param key The key or mouse button to hold + * @param ticks The number of ticks to hold the key or mouse button for + * @see #holdKey(InputUtil.Key) + */ + void holdKeyFor(InputUtil.Key key, int ticks); + + /** + * Holds a key for the specified number of ticks and then releases it. Waits until this process is finished. + * + * @param keyCode The key code of the key to hold + * @param ticks The number of ticks to hold the key for + * @see #holdKey(int) + */ + void holdKeyFor(int keyCode, int ticks); + + /** + * Holds a mouse button for the specified number of ticks and then releases it. Waits until this process is + * finished. + * + * @param button The mouse button to hold + * @param ticks The number of ticks to hold the mouse button for + * @see #holdMouse(int) + */ + void holdMouseFor(int button, int ticks); + + /** + * Types a code point (character). Useful for typing in text boxes. + * + * @param codePoint The code point to type + * @see #typeChars(String) + */ void typeChar(int codePoint); + /** + * Types a sequence of code points (characters) one after the other. Useful for typing in text boxes. + * + * @param chars The code points to type + */ void typeChars(String chars); + /** + * Scrolls the mouse vertically. + * + * @param amount The amount to scroll by + * @see #scroll(double, double) + */ void scroll(double amount); + /** + * Scrolls the mouse horizontally and vertically. + * + * @param xAmount The horizontal amount to scroll by + * @param yAmount The vertical amount to scroll by + * @see #scroll(double) + */ void scroll(double xAmount, double yAmount); + /** + * Sets the cursor position. + * + * @param x The x position of the new cursor position + * @param y The y position of the new cursor position + * @see #moveCursor(double, double) + */ void setCursorPos(double x, double y); + /** + * Moves the cursor position. + * + * @param deltaX The amount to add to the x position of the cursor + * @param deltaY The amount to add to the y position of the cursor + * @see #setCursorPos(double, double) + */ void moveCursor(double deltaX, double deltaY); } diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/ClientGameTestInputImpl.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/ClientGameTestInputImpl.java index 0590cd920f..ea24b1b665 100644 --- a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/ClientGameTestInputImpl.java +++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/ClientGameTestInputImpl.java @@ -54,25 +54,25 @@ public void clearKeysDown() { } @Override - public void pressKey(KeyBinding keyBinding) { - ThreadingImpl.checkOnGametestThread("pressKey"); + public void holdKey(KeyBinding keyBinding) { + ThreadingImpl.checkOnGametestThread("holdKey"); Preconditions.checkNotNull(keyBinding, "keyBinding"); - pressKey(getBoundKey(keyBinding, "press")); + holdKey(getBoundKey(keyBinding, "hold")); } @Override - public void pressKey(Function keyBindingGetter) { - ThreadingImpl.checkOnGametestThread("pressKey"); + public void holdKey(Function keyBindingGetter) { + ThreadingImpl.checkOnGametestThread("holdKey"); Preconditions.checkNotNull(keyBindingGetter, "keyBindingGetter"); KeyBinding keyBinding = context.computeOnClient(client -> keyBindingGetter.apply(client.options)); - pressKey(keyBinding); + holdKey(keyBinding); } @Override - public void pressKey(InputUtil.Key key) { - ThreadingImpl.checkOnGametestThread("pressKey"); + public void holdKey(InputUtil.Key key) { + ThreadingImpl.checkOnGametestThread("holdKey"); Preconditions.checkNotNull(key, "key"); if (KEYS_DOWN.add(key)) { @@ -81,38 +81,38 @@ public void pressKey(InputUtil.Key key) { } @Override - public void pressKey(int keyCode) { - ThreadingImpl.checkOnGametestThread("pressKey"); + public void holdKey(int keyCode) { + ThreadingImpl.checkOnGametestThread("holdKey"); - pressKey(InputUtil.Type.KEYSYM.createFromCode(keyCode)); + holdKey(InputUtil.Type.KEYSYM.createFromCode(keyCode)); } @Override - public void pressMouse(int button) { - ThreadingImpl.checkOnGametestThread("pressMouse"); + public void holdMouse(int button) { + ThreadingImpl.checkOnGametestThread("holdMouse"); - pressKey(InputUtil.Type.MOUSE.createFromCode(button)); + holdKey(InputUtil.Type.MOUSE.createFromCode(button)); } @Override - public void pressControl() { - ThreadingImpl.checkOnGametestThread("pressControl"); + public void holdControl() { + ThreadingImpl.checkOnGametestThread("holdControl"); - pressKey(MinecraftClient.IS_SYSTEM_MAC ? InputUtil.GLFW_KEY_LEFT_SUPER : InputUtil.GLFW_KEY_LEFT_CONTROL); + holdKey(MinecraftClient.IS_SYSTEM_MAC ? InputUtil.GLFW_KEY_LEFT_SUPER : InputUtil.GLFW_KEY_LEFT_CONTROL); } @Override - public void pressShift() { - ThreadingImpl.checkOnGametestThread("pressShift"); + public void holdShift() { + ThreadingImpl.checkOnGametestThread("holdShift"); - pressKey(InputUtil.GLFW_KEY_LEFT_SHIFT); + holdKey(InputUtil.GLFW_KEY_LEFT_SHIFT); } @Override - public void pressAlt() { - ThreadingImpl.checkOnGametestThread("pressAlt"); + public void holdAlt() { + ThreadingImpl.checkOnGametestThread("holdAlt"); - pressKey(InputUtil.GLFW_KEY_LEFT_ALT); + holdKey(InputUtil.GLFW_KEY_LEFT_ALT); } @Override @@ -186,89 +186,89 @@ private static void pressOrReleaseKey(MinecraftClient client, InputUtil.Key key, } @Override - public void pressReleaseKey(KeyBinding keyBinding) { - ThreadingImpl.checkOnGametestThread("pressReleaseKey"); + public void pressKey(KeyBinding keyBinding) { + ThreadingImpl.checkOnGametestThread("pressKey"); Preconditions.checkNotNull(keyBinding, "keyBinding"); - pressReleaseKey(getBoundKey(keyBinding, "press and release")); + pressKey(getBoundKey(keyBinding, "press")); } @Override - public void pressReleaseKey(Function keyBindingGetter) { - ThreadingImpl.checkOnGametestThread("pressReleaseKey"); + public void pressKey(Function keyBindingGetter) { + ThreadingImpl.checkOnGametestThread("pressKey"); Preconditions.checkNotNull(keyBindingGetter, "keyBindingGetter"); KeyBinding keyBinding = context.computeOnClient(client -> keyBindingGetter.apply(client.options)); - pressReleaseKey(keyBinding); + pressKey(keyBinding); } @Override - public void pressReleaseKey(InputUtil.Key key) { - ThreadingImpl.checkOnGametestThread("pressReleaseKey"); + public void pressKey(InputUtil.Key key) { + ThreadingImpl.checkOnGametestThread("pressKey"); Preconditions.checkNotNull(key, "key"); - pressKey(key); + holdKey(key); releaseKey(key); } @Override - public void pressReleaseKey(int keyCode) { - ThreadingImpl.checkOnGametestThread("pressReleaseKey"); + public void pressKey(int keyCode) { + ThreadingImpl.checkOnGametestThread("pressKey"); - pressReleaseKey(InputUtil.Type.KEYSYM.createFromCode(keyCode)); + pressKey(InputUtil.Type.KEYSYM.createFromCode(keyCode)); } @Override - public void pressReleaseMouse(int button) { - ThreadingImpl.checkOnGametestThread("pressReleaseMouse"); + public void pressMouse(int button) { + ThreadingImpl.checkOnGametestThread("pressMouse"); - pressReleaseKey(InputUtil.Type.MOUSE.createFromCode(button)); + pressKey(InputUtil.Type.MOUSE.createFromCode(button)); } @Override - public void holdKey(KeyBinding keyBinding, int ticks) { - ThreadingImpl.checkOnGametestThread("holdKey"); + public void holdKeyFor(KeyBinding keyBinding, int ticks) { + ThreadingImpl.checkOnGametestThread("holdKeyFor"); Preconditions.checkNotNull(keyBinding, "keyBinding"); Preconditions.checkArgument(ticks > 0, "ticks must be positive"); - holdKey(getBoundKey(keyBinding, "hold"), ticks); + holdKeyFor(getBoundKey(keyBinding, "hold"), ticks); } @Override - public void holdKey(Function keyBindingGetter, int ticks) { - ThreadingImpl.checkOnGametestThread("holdKey"); + public void holdKeyFor(Function keyBindingGetter, int ticks) { + ThreadingImpl.checkOnGametestThread("holdKeyFor"); Preconditions.checkNotNull(keyBindingGetter, "keyBindingGetter"); Preconditions.checkArgument(ticks > 0, "ticks must be positive"); KeyBinding keyBinding = context.computeOnClient(client -> keyBindingGetter.apply(client.options)); - holdKey(keyBinding, ticks); + holdKeyFor(keyBinding, ticks); } @Override - public void holdKey(InputUtil.Key key, int ticks) { - ThreadingImpl.checkOnGametestThread("holdKey"); + public void holdKeyFor(InputUtil.Key key, int ticks) { + ThreadingImpl.checkOnGametestThread("holdKeyFor"); Preconditions.checkNotNull(key, "key"); Preconditions.checkArgument(ticks > 0, "ticks must be positive"); - pressKey(key); + holdKey(key); context.waitTicks(ticks); releaseKey(key); } @Override - public void holdKey(int keyCode, int ticks) { - ThreadingImpl.checkOnGametestThread("holdKey"); + public void holdKeyFor(int keyCode, int ticks) { + ThreadingImpl.checkOnGametestThread("holdKeyFor"); Preconditions.checkArgument(ticks > 0, "ticks must be positive"); - holdKey(InputUtil.Type.KEYSYM.createFromCode(keyCode), ticks); + holdKeyFor(InputUtil.Type.KEYSYM.createFromCode(keyCode), ticks); } @Override - public void holdMouse(int button, int ticks) { - ThreadingImpl.checkOnGametestThread("holdMouse"); + public void holdMouseFor(int button, int ticks) { + ThreadingImpl.checkOnGametestThread("holdMouseFor"); Preconditions.checkArgument(ticks > 0, "ticks must be positive"); - holdKey(InputUtil.Type.MOUSE.createFromCode(button), ticks); + holdKeyFor(InputUtil.Type.MOUSE.createFromCode(button), ticks); } @Override diff --git a/fabric-client-gametest-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/gametest/client/ClientGameTestTest.java b/fabric-client-gametest-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/gametest/client/ClientGameTestTest.java index 4460826cef..7e8fe43dc1 100644 --- a/fabric-client-gametest-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/gametest/client/ClientGameTestTest.java +++ b/fabric-client-gametest-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/gametest/client/ClientGameTestTest.java @@ -82,10 +82,10 @@ public void runTest(ClientGameTestContext context) { } { - context.getInput().pressReleaseKey(options -> options.chatKey); + context.getInput().pressKey(options -> options.chatKey); context.waitTick(); context.getInput().typeChars("Hello, World!"); - context.getInput().pressReleaseKey(InputUtil.GLFW_KEY_ENTER); + context.getInput().pressKey(InputUtil.GLFW_KEY_ENTER); context.takeScreenshot("chat_message_sent", 5); } @@ -99,7 +99,7 @@ public void runTest(ClientGameTestContext context) { } { - context.getInput().pressReleaseKey(options -> options.inventoryKey); + context.getInput().pressKey(options -> options.inventoryKey); context.takeScreenshot("in_game_inventory"); context.setScreen(null); } From dddfaa140a87f95f766ee497698ac516615e6183 Mon Sep 17 00:00:00 2001 From: joe Date: Wed, 11 Dec 2024 15:54:17 +0000 Subject: [PATCH 08/15] Rename client gametest github action --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4422c2ee35..f92e6d7730 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -43,7 +43,7 @@ jobs: with: distribution: 'microsoft' java-version: '21' - - name: Run Auto test Client + - name: Run Client Gametests uses: modmuss50/xvfb-action@v1 with: run: ./gradlew runProductionClientGametest --stacktrace --warning-mode=fail From 686d7fa5fe3b9e7c36dd2b88d9192365453a7243 Mon Sep 17 00:00:00 2001 From: joe Date: Wed, 11 Dec 2024 20:16:11 +0000 Subject: [PATCH 09/15] Fix tryClickScreenButtonImpl for buttons inside layout widgets --- .../client/gametest/ClientGameTestContextImpl.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/ClientGameTestContextImpl.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/ClientGameTestContextImpl.java index 8dfaaa2266..854bb16970 100644 --- a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/ClientGameTestContextImpl.java +++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/ClientGameTestContextImpl.java @@ -24,6 +24,7 @@ import com.google.common.base.Preconditions; import org.apache.commons.lang3.function.FailableConsumer; import org.apache.commons.lang3.function.FailableFunction; +import org.apache.commons.lang3.mutable.MutableBoolean; import org.apache.commons.lang3.mutable.MutableObject; import org.jetbrains.annotations.Nullable; @@ -200,7 +201,16 @@ private static boolean tryClickScreenButtonImpl(@Nullable Screen screen, String } if (drawable instanceof Widget widget) { - widget.forEachChild(clickableWidget -> pressMatchingButton(clickableWidget, buttonText)); + MutableBoolean found = new MutableBoolean(false); + widget.forEachChild(clickableWidget -> { + if (!found.booleanValue()) { + found.setValue(pressMatchingButton(clickableWidget, buttonText)); + } + }); + + if (found.booleanValue()) { + return true; + } } } From b6b4ffb30b3b51321664e17e4edcca03390ca0e4 Mon Sep 17 00:00:00 2001 From: joe Date: Wed, 11 Dec 2024 23:40:48 +0000 Subject: [PATCH 10/15] Address review comments and repackage client gametest test --- .../gametest/v1/ClientGameTestInput.java | 42 ++++++++++++------- .../gametest/ClientGameTestInputImpl.java | 2 +- .../fabric-client-gametest-api-v1.mixins.json | 2 +- .../gametest}/ClientGameTestTest.java | 10 ++++- .../gametest/mixin}/TitleScreenAccessor.java | 2 +- ...ic-client-gametest-api-v1-test.mixins.json | 14 +++++++ .../testmodClient/resources/fabric.mod.json | 7 +++- 7 files changed, 57 insertions(+), 22 deletions(-) rename fabric-client-gametest-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/{gametest/client => client/gametest}/ClientGameTestTest.java (94%) rename fabric-client-gametest-api-v1/src/{client/java/net/fabricmc/fabric/mixin/client/gametest => testmodClient/java/net/fabricmc/fabric/test/client/gametest/mixin}/TitleScreenAccessor.java (93%) create mode 100644 fabric-client-gametest-api-v1/src/testmodClient/resources/fabric-client-gametest-api-v1-test.mixins.json diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/ClientGameTestInput.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/ClientGameTestInput.java index 90753d1dd1..5b116185d1 100644 --- a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/ClientGameTestInput.java +++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/ClientGameTestInput.java @@ -18,6 +18,8 @@ import java.util.function.Function; +import org.jetbrains.annotations.ApiStatus; + import net.minecraft.client.gui.screen.Screen; import net.minecraft.client.option.GameOptions; import net.minecraft.client.option.KeyBinding; @@ -26,10 +28,11 @@ /** * The client gametest input handler used to simulate inputs to the client. */ +@ApiStatus.NonExtendable public interface ClientGameTestInput { /** * Starts holding down a key binding. The key binding will be held until it is released. The key binding must be - * bound. + * bound. Does nothing if the key binding is already being held. * * @param keyBinding The key binding to hold * @see #releaseKey(KeyBinding) @@ -40,7 +43,7 @@ public interface ClientGameTestInput { /** * Starts holding down a key binding. The key binding will be held until it is released. The key binding must be - * bound. + * bound. Does nothing if the key binding is already being held. * * @param keyBindingGetter The function to get the key binding from the game options * @see #releaseKey(Function) @@ -50,7 +53,8 @@ public interface ClientGameTestInput { void holdKey(Function keyBindingGetter); /** - * Starts holding down a key or mouse button. The key will be held until it is released. + * Starts holding down a key or mouse button. The key will be held until it is released. Does nothing if the key or + * mouse button is already being held. * * @param key The key or mouse button to hold * @see #releaseKey(InputUtil.Key) @@ -59,7 +63,8 @@ public interface ClientGameTestInput { void holdKey(InputUtil.Key key); /** - * Starts holding down a key. The key will be held until it is released. + * Starts holding down a key. The key will be held until it is released. Does nothing if the key is already being + * held. * * @param keyCode The key code of the key to hold * @see #releaseKey(int) @@ -68,7 +73,8 @@ public interface ClientGameTestInput { void holdKey(int keyCode); /** - * Starts holding down a mouse button. The mouse button will be held until it is released. + * Starts holding down a mouse button. The mouse button will be held until it is released. Does nothing if the mouse + * button is already being held. * * @param button The mouse button to hold * @see #releaseMouse(int) @@ -78,7 +84,8 @@ public interface ClientGameTestInput { /** * Starts holding down left control, or left super on macOS. Suitable for triggering - * {@link Screen#hasControlDown()}. The key will be held until it is released. + * {@link Screen#hasControlDown()}. The key will be held until it is released. Does nothing if the key is already + * being held. * * @see #releaseControl() */ @@ -86,7 +93,7 @@ public interface ClientGameTestInput { /** * Starts holding down left shift. Suitable for triggering {@link Screen#hasShiftDown()}. The key will be held until - * it is released. + * it is released. Does nothing if the key is already being held. * * @see #releaseShift() */ @@ -94,14 +101,14 @@ public interface ClientGameTestInput { /** * Starts holding down left alt. Suitable for triggering {@link Screen#hasAltDown()}. The key will be held until it - * is released. + * is released. Does nothing if the key is already being held. * * @see #releaseAlt() */ void holdAlt(); /** - * Releases a key binding. The key binding must be bound. + * Releases a key binding. The key binding must be bound. Does nothing if the key binding is not being held. * * @param keyBinding The key binding to release * @see #holdKey(KeyBinding) @@ -110,7 +117,7 @@ public interface ClientGameTestInput { void releaseKey(KeyBinding keyBinding); /** - * Releases a key binding. The key binding must be bound. + * Releases a key binding. The key binding must be bound. Does nothing if the key binding is not being held. * * @param keyBindingGetter The function to get the key binding from the game options * @see #holdKey(Function) @@ -119,7 +126,7 @@ public interface ClientGameTestInput { void releaseKey(Function keyBindingGetter); /** - * Releases a key or mouse button. + * Releases a key or mouse button. Does nothing if the key or mouse button is not being held. * * @param key The key or mouse button to release * @see #holdKey(InputUtil.Key) @@ -127,7 +134,7 @@ public interface ClientGameTestInput { void releaseKey(InputUtil.Key key); /** - * Releases a key. + * Releases a key. Does nothing if the key is not being held. * * @param keyCode The key code of the key to release * @see #holdKey(int) @@ -135,7 +142,7 @@ public interface ClientGameTestInput { void releaseKey(int keyCode); /** - * Releases a mouse button. + * Releases a mouse button. Does nothing if the mouse button is not being held. * * @param button The mouse button to release * @see #holdMouse(int) @@ -143,21 +150,24 @@ public interface ClientGameTestInput { void releaseMouse(int button); /** - * Releases left control, or left super on macOS. Suitable for un-triggering {@link Screen#hasControlDown()}. + * Releases left control, or left super on macOS. Suitable for un-triggering {@link Screen#hasControlDown()}. Does + * nothing if the key is not being held. * * @see #holdControl() */ void releaseControl(); /** - * Releases left shift. Suitable for un-triggering {@link Screen#hasShiftDown()}. + * Releases left shift. Suitable for un-triggering {@link Screen#hasShiftDown()}. Does nothing if the key is not + * being held. * * @see #holdShift() */ void releaseShift(); /** - * Releases left alt. Suitable for un-triggering {@link Screen#hasAltDown()}. + * Releases left alt. Suitable for un-triggering {@link Screen#hasAltDown()}. Does nothing if the key is not being + * held. * * @see #holdAlt() */ diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/ClientGameTestInputImpl.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/ClientGameTestInputImpl.java index ea24b1b665..f957604411 100644 --- a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/ClientGameTestInputImpl.java +++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/ClientGameTestInputImpl.java @@ -325,7 +325,7 @@ private static InputUtil.Key getBoundKey(KeyBinding keyBinding, String action) { InputUtil.Key boundKey = ((KeyBindingAccessor) keyBinding).getBoundKey(); if (boundKey == InputUtil.UNKNOWN_KEY) { - throw new AssertionError("Cannot %s binding '%s' because it isn'keyBinding bound to a key".formatted(action, keyBinding.getTranslationKey())); + throw new AssertionError("Cannot %s binding '%s' because it isn't bound to a key".formatted(action, keyBinding.getTranslationKey())); } return boundKey; diff --git a/fabric-client-gametest-api-v1/src/client/resources/fabric-client-gametest-api-v1.mixins.json b/fabric-client-gametest-api-v1/src/client/resources/fabric-client-gametest-api-v1.mixins.json index 6afc114a99..d702411058 100644 --- a/fabric-client-gametest-api-v1/src/client/resources/fabric-client-gametest-api-v1.mixins.json +++ b/fabric-client-gametest-api-v1/src/client/resources/fabric-client-gametest-api-v1.mixins.json @@ -20,7 +20,7 @@ "MinecraftClientMixin", "MouseAccessor", "ScreenAccessor", - "TitleScreenAccessor", + "net.fabricmc.fabric.test.gametest.client.mixin.TitleScreenAccessor", "WindowMixin" ] } diff --git a/fabric-client-gametest-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/gametest/client/ClientGameTestTest.java b/fabric-client-gametest-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/client/gametest/ClientGameTestTest.java similarity index 94% rename from fabric-client-gametest-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/gametest/client/ClientGameTestTest.java rename to fabric-client-gametest-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/client/gametest/ClientGameTestTest.java index 7e8fe43dc1..9a75279406 100644 --- a/fabric-client-gametest-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/gametest/client/ClientGameTestTest.java +++ b/fabric-client-gametest-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/client/gametest/ClientGameTestTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.fabricmc.fabric.test.gametest.client; +package net.fabricmc.fabric.test.client.gametest; import java.io.IOException; import java.io.UncheckedIOException; @@ -46,11 +46,13 @@ import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest; import net.fabricmc.fabric.impl.client.gametest.TestDedicatedServer; import net.fabricmc.fabric.impl.client.gametest.ThreadingImpl; +import net.fabricmc.fabric.test.client.gametest.mixin.TitleScreenAccessor; import net.fabricmc.loader.api.FabricLoader; public class ClientGameTestTest implements FabricClientGameTest { public void runTest(ClientGameTestContext context) { { + waitForTitleScreenFade(context); context.takeScreenshot("title_screen", 0); context.clickScreenButton("menu.singleplayer"); } @@ -153,6 +155,12 @@ private static boolean isDirEmpty(Path path) { } } + private static void waitForTitleScreenFade(ClientGameTestContext context) { + context.waitFor(client -> { + return !(client.currentScreen instanceof TitleScreenAccessor titleScreen) || !titleScreen.getDoBackgroundFade(); + }); + } + private static void enableDebugHud(ClientGameTestContext context) { context.runOnClient(client -> client.inGameHud.getDebugHud().toggleDebugHud()); } diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/TitleScreenAccessor.java b/fabric-client-gametest-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/client/gametest/mixin/TitleScreenAccessor.java similarity index 93% rename from fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/TitleScreenAccessor.java rename to fabric-client-gametest-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/client/gametest/mixin/TitleScreenAccessor.java index 654a0092f4..269795921f 100644 --- a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/TitleScreenAccessor.java +++ b/fabric-client-gametest-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/client/gametest/mixin/TitleScreenAccessor.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package net.fabricmc.fabric.mixin.client.gametest; +package net.fabricmc.fabric.test.client.gametest.mixin; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.gen.Accessor; diff --git a/fabric-client-gametest-api-v1/src/testmodClient/resources/fabric-client-gametest-api-v1-test.mixins.json b/fabric-client-gametest-api-v1/src/testmodClient/resources/fabric-client-gametest-api-v1-test.mixins.json new file mode 100644 index 0000000000..03ab51de46 --- /dev/null +++ b/fabric-client-gametest-api-v1/src/testmodClient/resources/fabric-client-gametest-api-v1-test.mixins.json @@ -0,0 +1,14 @@ +{ + "required": true, + "package": "net.fabricmc.fabric.test.client.gametest.mixin", + "compatibilityLevel": "JAVA_21", + "mixins": [ + ], + "plugin": "net.fabricmc.fabric.impl.client.gametest.ClientGameTestMixinConfigPlugin", + "injectors": { + "defaultRequire": 1 + }, + "client": [ + "TitleScreenAccessor" + ] +} diff --git a/fabric-client-gametest-api-v1/src/testmodClient/resources/fabric.mod.json b/fabric-client-gametest-api-v1/src/testmodClient/resources/fabric.mod.json index e1e761f4bb..e41d7a932b 100644 --- a/fabric-client-gametest-api-v1/src/testmodClient/resources/fabric.mod.json +++ b/fabric-client-gametest-api-v1/src/testmodClient/resources/fabric.mod.json @@ -7,7 +7,10 @@ "license": "Apache-2.0", "entrypoints": { "fabric-client-gametest": [ - "net.fabricmc.fabric.test.gametest.client.ClientGameTestTest" + "net.fabricmc.fabric.test.client.gametest.ClientGameTestTest" ] - } + }, + "mixins": [ + "fabric-client-gametest-api-v1-test.mixins.json" + ] } From e9d4af26c815e9110fc1f7324f50ae14d3a6f0f0 Mon Sep 17 00:00:00 2001 From: joe Date: Wed, 11 Dec 2024 23:42:25 +0000 Subject: [PATCH 11/15] Delete wrong reference to TitleScreenAccessor. Thanks mcdev --- .../client/resources/fabric-client-gametest-api-v1.mixins.json | 1 - 1 file changed, 1 deletion(-) diff --git a/fabric-client-gametest-api-v1/src/client/resources/fabric-client-gametest-api-v1.mixins.json b/fabric-client-gametest-api-v1/src/client/resources/fabric-client-gametest-api-v1.mixins.json index d702411058..8b906a7950 100644 --- a/fabric-client-gametest-api-v1/src/client/resources/fabric-client-gametest-api-v1.mixins.json +++ b/fabric-client-gametest-api-v1/src/client/resources/fabric-client-gametest-api-v1.mixins.json @@ -20,7 +20,6 @@ "MinecraftClientMixin", "MouseAccessor", "ScreenAccessor", - "net.fabricmc.fabric.test.gametest.client.mixin.TitleScreenAccessor", "WindowMixin" ] } From beb47a069ea5461bdd0bf50b349acfa8d2e740b8 Mon Sep 17 00:00:00 2001 From: joe Date: Thu, 12 Dec 2024 15:46:39 +0000 Subject: [PATCH 12/15] Address review comments --- .../gametest/v1/ClientGameTestContext.java | 8 +++-- .../gametest/v1/FabricClientGameTest.java | 2 +- .../gametest/ClientGameTestContextImpl.java | 14 +++++--- .../gametest/FabricClientGameTestRunner.java | 33 ++++++++++--------- .../fabric-client-gametest-api-v1.mixins.json | 16 ++++----- .../client/gametest/ClientGameTestTest.java | 6 ++-- 6 files changed, 42 insertions(+), 37 deletions(-) diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/ClientGameTestContext.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/ClientGameTestContext.java index c02f961c4b..9b339d08d6 100644 --- a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/ClientGameTestContext.java +++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/ClientGameTestContext.java @@ -16,7 +16,9 @@ package net.fabricmc.fabric.api.client.gametest.v1; +import java.nio.file.Path; import java.util.function.Predicate; +import java.util.function.Supplier; import org.apache.commons.lang3.function.FailableConsumer; import org.apache.commons.lang3.function.FailableFunction; @@ -85,7 +87,7 @@ public interface ClientGameTestContext { * @param screen The screen to open * @see MinecraftClient#setScreen(Screen) */ - void setScreen(@Nullable Screen screen); + void setScreen(Supplier<@Nullable Screen> screen); /** * Presses the button in the current screen whose label is the given translation key. Fails if the button couldn't @@ -109,7 +111,7 @@ public interface ClientGameTestContext { * * @param name The name of the screenshot */ - void takeScreenshot(String name); + Path takeScreenshot(String name); /** * Takes a screnshot after waiting {@code delay} ticks and saves it in the screenshots directory. @@ -117,7 +119,7 @@ public interface ClientGameTestContext { * @param name The name of the screenshot * @param delay The delay in ticks before taking the screenshot */ - void takeScreenshot(String name, int delay); + Path takeScreenshot(String name, int delay); /** * Gets the input handler used to simulate inputs to the client. diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/FabricClientGameTest.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/FabricClientGameTest.java index f59de17b5b..d0448e0b46 100644 --- a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/FabricClientGameTest.java +++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/FabricClientGameTest.java @@ -17,7 +17,7 @@ package net.fabricmc.fabric.api.client.gametest.v1; /** - * The client game test entrypoint interface. See the package documentation. + * The {@code fabric-client-gametest} entrypoint interface. See the package documentation. */ public interface FabricClientGameTest { /** diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/ClientGameTestContextImpl.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/ClientGameTestContextImpl.java index 854bb16970..acb04c842e 100644 --- a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/ClientGameTestContextImpl.java +++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/ClientGameTestContextImpl.java @@ -16,10 +16,12 @@ package net.fabricmc.fabric.impl.client.gametest; +import java.nio.file.Path; import java.util.HashMap; import java.util.Map; import java.util.function.Function; import java.util.function.Predicate; +import java.util.function.Supplier; import com.google.common.base.Preconditions; import org.apache.commons.lang3.function.FailableConsumer; @@ -159,9 +161,9 @@ public void waitForScreen(@Nullable Class screenClass) { } @Override - public void setScreen(@Nullable Screen screen) { + public void setScreen(Supplier<@Nullable Screen> screen) { ThreadingImpl.checkOnGametestThread("setScreen"); - runOnClient(client -> client.setScreen(screen)); + runOnClient(client -> client.setScreen(screen.get())); } @Override @@ -239,14 +241,14 @@ private static boolean pressMatchingButton(ClickableWidget widget, String text) } @Override - public void takeScreenshot(String name) { + public Path takeScreenshot(String name) { ThreadingImpl.checkOnGametestThread("takeScreenshot"); Preconditions.checkNotNull(name, "name"); - takeScreenshot(name, 1); + return takeScreenshot(name, 1); } @Override - public void takeScreenshot(String name, int delay) { + public Path takeScreenshot(String name, int delay) { ThreadingImpl.checkOnGametestThread("takeScreenshot"); Preconditions.checkNotNull(name, "name"); Preconditions.checkArgument(delay >= 0, "delay cannot be negative"); @@ -256,6 +258,8 @@ public void takeScreenshot(String name, int delay) { ScreenshotRecorder.saveScreenshot(FabricLoader.getInstance().getGameDir().toFile(), name + ".png", client.getFramebuffer(), (message) -> { }); }); + + return FabricLoader.getInstance().getGameDir().resolve("screenshots").resolve(name + ".png"); } @Override diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/FabricClientGameTestRunner.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/FabricClientGameTestRunner.java index 433d762a21..127a21f010 100644 --- a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/FabricClientGameTestRunner.java +++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/FabricClientGameTestRunner.java @@ -18,17 +18,14 @@ import java.util.List; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import net.minecraft.client.MinecraftClient; import net.minecraft.client.gui.screen.TitleScreen; +import net.fabricmc.fabric.api.client.gametest.v1.ClientGameTestContext; import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest; import net.fabricmc.loader.api.FabricLoader; public class FabricClientGameTestRunner { - private static final Logger LOGGER = LoggerFactory.getLogger("fabric-client-gametest-api-v1"); private static final String ENTRYPOINT_KEY = "fabric-client-gametest"; public static void start() { @@ -39,31 +36,35 @@ public static void start() { ThreadingImpl.runTestThread(() -> { ClientGameTestContextImpl context = new ClientGameTestContextImpl(); - boolean failed = false; for (FabricClientGameTest gameTest : gameTests) { context.restoreDefaultGameOptions(); try { gameTest.runTest(context); - } catch (Throwable e) { - LOGGER.error("Failed test {}", gameTest.getClass().getName(), e); - failed = true; } finally { context.getInput().clearKeysDown(); - - // Open the title screen to reset the state for the next gametest. - // If the gametest API was used correctly, we should be in the menus somewhere because any test - // world should have been closed at the end of a try-with-resources statement. - context.setScreen(new TitleScreen()); + checkFinalGameTestState(context, gameTest.getClass().getName()); } } - if (failed) { - throw new AssertionError("There were failing client gametests"); + context.clickScreenButton("menu.quit"); + }); + } + + private static void checkFinalGameTestState(ClientGameTestContext context, String testClassName) { + if (ThreadingImpl.isServerRunning) { + throw new AssertionError("Client gametest %s finished while a server is still running".formatted(testClassName)); + } + + context.runOnClient(client -> { + if (client.getNetworkHandler() != null) { + throw new AssertionError("Client gametest %s finished while still connected to a server".formatted(testClassName)); } - context.clickScreenButton("menu.quit"); + if (!(client.currentScreen instanceof TitleScreen)) { + throw new AssertionError("Client gametest %s did not finish on the title screen".formatted(testClassName)); + } }); } } diff --git a/fabric-client-gametest-api-v1/src/client/resources/fabric-client-gametest-api-v1.mixins.json b/fabric-client-gametest-api-v1/src/client/resources/fabric-client-gametest-api-v1.mixins.json index 8b906a7950..5b0dbb8113 100644 --- a/fabric-client-gametest-api-v1/src/client/resources/fabric-client-gametest-api-v1.mixins.json +++ b/fabric-client-gametest-api-v1/src/client/resources/fabric-client-gametest-api-v1.mixins.json @@ -3,14 +3,6 @@ "package": "net.fabricmc.fabric.mixin.client.gametest", "compatibilityLevel": "JAVA_21", "mixins": [ - "MinecraftDedicatedServerMixin", - "MinecraftServerMixin" - ], - "plugin": "net.fabricmc.fabric.impl.client.gametest.ClientGameTestMixinConfigPlugin", - "injectors": { - "defaultRequire": 1 - }, - "client": [ "CyclingButtonWidgetAccessor", "GameOptionsAccessor", "GameOptionsMixin", @@ -18,8 +10,14 @@ "KeyBindingAccessor", "KeyboardAccessor", "MinecraftClientMixin", + "MinecraftDedicatedServerMixin", + "MinecraftServerMixin", "MouseAccessor", "ScreenAccessor", "WindowMixin" - ] + ], + "plugin": "net.fabricmc.fabric.impl.client.gametest.ClientGameTestMixinConfigPlugin", + "injectors": { + "defaultRequire": 1 + } } diff --git a/fabric-client-gametest-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/client/gametest/ClientGameTestTest.java b/fabric-client-gametest-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/client/gametest/ClientGameTestTest.java index 9a75279406..07d684e2db 100644 --- a/fabric-client-gametest-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/client/gametest/ClientGameTestTest.java +++ b/fabric-client-gametest-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/client/gametest/ClientGameTestTest.java @@ -103,11 +103,11 @@ public void runTest(ClientGameTestContext context) { { context.getInput().pressKey(options -> options.inventoryKey); context.takeScreenshot("in_game_inventory"); - context.setScreen(null); + context.setScreen(() -> null); } { - context.setScreen(new GameMenuScreen(true)); + context.setScreen(() -> new GameMenuScreen(true)); context.takeScreenshot("game_menu"); context.clickScreenButton("menu.returnToMenu"); context.waitForScreen(TitleScreen.class); @@ -133,7 +133,7 @@ public void runTest(ClientGameTestContext context) { waitForWorldTicks(context, 1); } - context.setScreen(new GameMenuScreen(true)); + context.setScreen(() -> new GameMenuScreen(true)); context.takeScreenshot("server_game_menu"); context.clickScreenButton("menu.disconnect"); From 504ffcd87c6640134aa19124f92284f3e369d329 Mon Sep 17 00:00:00 2001 From: joe Date: Thu, 12 Dec 2024 15:51:02 +0000 Subject: [PATCH 13/15] Clean up default game options --- .../impl/client/gametest/ClientGameTestContextImpl.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/ClientGameTestContextImpl.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/ClientGameTestContextImpl.java index acb04c842e..3674b90b6a 100644 --- a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/ClientGameTestContextImpl.java +++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/ClientGameTestContextImpl.java @@ -38,6 +38,7 @@ import net.minecraft.client.gui.widget.CyclingButtonWidget; import net.minecraft.client.gui.widget.PressableWidget; import net.minecraft.client.gui.widget.Widget; +import net.minecraft.client.option.CloudRenderMode; import net.minecraft.client.option.GameOptions; import net.minecraft.client.option.SimpleOption; import net.minecraft.client.tutorial.TutorialStep; @@ -58,8 +59,14 @@ public final class ClientGameTestContextImpl implements ClientGameTestContext { private static final Map DEFAULT_GAME_OPTIONS = new HashMap<>(); public static void initGameOptions(GameOptions options) { - options.onboardAccessibility = false; + // Messes with the consistency of gametests options.tutorialStep = TutorialStep.NONE; + options.getCloudRenderMode().setValue(CloudRenderMode.OFF); + + // Messes with game tests starting + options.onboardAccessibility = false; + + // Just annoying options.getSoundVolumeOption(SoundCategory.MUSIC).setValue(0.0); ((GameOptionsAccessor) options).invokeAccept(new GameOptions.Visitor() { From 232743dab91fd93b7a3df2c455ec6cb00bad474b Mon Sep 17 00:00:00 2001 From: joe Date: Thu, 12 Dec 2024 16:36:23 +0000 Subject: [PATCH 14/15] Improve documentation --- .../gametest/v1/ClientGameTestContext.java | 2 +- .../gametest/v1/ClientGameTestInput.java | 22 ++++++++++++++----- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/ClientGameTestContext.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/ClientGameTestContext.java index 9b339d08d6..9c46076601 100644 --- a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/ClientGameTestContext.java +++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/ClientGameTestContext.java @@ -41,7 +41,7 @@ public interface ClientGameTestContext { int NO_TIMEOUT = -1; /** - * The default timeout for wait tasks. + * The default timeout in ticks for wait tasks (10 seconds). */ int DEFAULT_TIMEOUT = 10 * SharedConstants.TICKS_PER_SECOND; diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/ClientGameTestInput.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/ClientGameTestInput.java index 5b116185d1..5bbf839965 100644 --- a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/ClientGameTestInput.java +++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/ClientGameTestInput.java @@ -136,7 +136,7 @@ public interface ClientGameTestInput { /** * Releases a key. Does nothing if the key is not being held. * - * @param keyCode The key code of the key to release + * @param keyCode The GLFW key code of the key to release * @see #holdKey(int) */ void releaseKey(int keyCode); @@ -144,7 +144,7 @@ public interface ClientGameTestInput { /** * Releases a mouse button. Does nothing if the mouse button is not being held. * - * @param button The mouse button to release + * @param button The GLFW mouse button to release * @see #holdMouse(int) */ void releaseMouse(int button); @@ -202,7 +202,10 @@ public interface ClientGameTestInput { /** * Presses and releases a key. * - * @param keyCode The key code of the key to press + *

For sending Unicode text input (e.g. into text boxes), use {@link #typeChar(int)} or + * {@link #typeChars(String)} instead. + * + * @param keyCode The GLFW key code of the key to press * @see #holdKey(int) */ void pressKey(int keyCode); @@ -210,7 +213,7 @@ public interface ClientGameTestInput { /** * Presses and releases a mouse button. * - * @param button The mouse button to press + * @param button The GLFW mouse button to press * @see #holdMouse(int) */ void pressMouse(int button); @@ -250,7 +253,7 @@ public interface ClientGameTestInput { /** * Holds a key for the specified number of ticks and then releases it. Waits until this process is finished. * - * @param keyCode The key code of the key to hold + * @param keyCode The GLFW key code of the key to hold * @param ticks The number of ticks to hold the key for * @see #holdKey(int) */ @@ -260,7 +263,7 @@ public interface ClientGameTestInput { * Holds a mouse button for the specified number of ticks and then releases it. Waits until this process is * finished. * - * @param button The mouse button to hold + * @param button The GLFW mouse button to hold * @param ticks The number of ticks to hold the mouse button for * @see #holdMouse(int) */ @@ -269,8 +272,15 @@ public interface ClientGameTestInput { /** * Types a code point (character). Useful for typing in text boxes. * + *

This method is for sending Unicode text input, not for pressing keys on the keyboard for other + * purposes, such as pressing {@code W} for moving the player. For those use cases, use one of the {@code pressKey} + * overloads instead. + * * @param codePoint The code point to type * @see #typeChars(String) + * @see #pressKey(int) + * @see #pressKey(KeyBinding) + * @see #pressKey(Function) */ void typeChar(int codePoint); From 0890509a5aa89898c3b27e1e94a4dffe0d9717a5 Mon Sep 17 00:00:00 2001 From: joe Date: Fri, 13 Dec 2024 15:41:52 +0000 Subject: [PATCH 15/15] Remove module dependencies --- fabric-client-gametest-api-v1/build.gradle | 5 ----- 1 file changed, 5 deletions(-) diff --git a/fabric-client-gametest-api-v1/build.gradle b/fabric-client-gametest-api-v1/build.gradle index c15dd4930d..3a7a5eb6e8 100644 --- a/fabric-client-gametest-api-v1/build.gradle +++ b/fabric-client-gametest-api-v1/build.gradle @@ -3,8 +3,3 @@ version = getSubprojectVersion(project) loom { accessWidenerPath = file('src/client/resources/fabric-client-gametest-api-v1.accesswidener') } - -moduleDependencies(project, [ - 'fabric-api-base', - 'fabric-resource-loader-v0' -])