mirror of
https://github.com/FabricMC/fabric.git
synced 2025-04-21 03:10:54 -04:00
Add experimental Client Game Test API (#4292)
* Move client auto tests to new module fabric-client-gametest-api-v1 * Fix run prod Gradle tasks * Rename remaining references to client auto-tests * Switch client gametests to using entrypoints * Disable input and cursor grabbing * Remove FabricClientTestHelper moving most of it into the context. Add the ability to simulate key and mouse inputs * Rename and document input methods * Rename client gametest github action * Fix tryClickScreenButtonImpl for buttons inside layout widgets * Address review comments and repackage client gametest test * Delete wrong reference to TitleScreenAccessor. Thanks mcdev * Address review comments * Clean up default game options * Improve documentation * Remove module dependencies --------- Co-authored-by: modmuss50 <modmuss50@gmail.com>
This commit is contained in:
parent
08f5ef8bb2
commit
b47eab6b15
39 changed files with 1983 additions and 550 deletions
.github/workflows
build.gradlefabric-api-base/src
testmod/resources
testmodClient
java/net/fabricmc/fabric/test/base/client
resources
fabric-client-gametest-api-v1
build.gradle
gradle.propertiessettings.gradlesrc
client
java/net/fabricmc/fabric
api/client/gametest/v1
impl/client/gametest
ClientGameTestContextImpl.javaClientGameTestInputImpl.javaClientGameTestMixinConfigPlugin.javaFabricClientGameTestRunner.javaTestDedicatedServer.javaThreadingImpl.java
mixin/client/gametest
resources
assets/fabric-client-gametest-api-v1
fabric-client-gametest-api-v1.accesswidenerfabric-client-gametest-api-v1.mixins.jsonfabric.mod.jsontestmodClient
java/net/fabricmc/fabric/test/client/gametest
resources
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
|
@ -43,10 +43,10 @@ jobs:
|
|||
with:
|
||||
distribution: 'microsoft'
|
||||
java-version: '21'
|
||||
- name: Run Auto test Client
|
||||
- name: Run Client Gametests
|
||||
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:
|
||||
|
|
37
build.gradle
37
build.gradle
|
@ -417,10 +417,10 @@ loom {
|
|||
name "Auto Test Server"
|
||||
vmArg "-Dfabric.autoTest"
|
||||
}
|
||||
autoTestClient {
|
||||
clientGametest {
|
||||
inherit testmodClient
|
||||
name "Auto Test Client"
|
||||
vmArg "-Dfabric.autoTest"
|
||||
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 {
|
||||
|
@ -484,6 +484,9 @@ configurations {
|
|||
extendsFrom configurations.minecraftRuntimeLibraries
|
||||
}
|
||||
productionRuntimeServer
|
||||
productionMods {
|
||||
transitive = false
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
@ -491,13 +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
|
||||
tasks.register('runProductionClientGametest', JavaExec) {
|
||||
dependsOn productionMods, downloadAssets
|
||||
classpath.from configurations.productionRuntime
|
||||
mainClass = "net.fabricmc.loader.impl.launch.knot.KnotClient"
|
||||
workingDir = file("run")
|
||||
|
@ -519,8 +525,8 @@ tasks.register('runProductionAutoTestClient', JavaExec) {
|
|||
}
|
||||
|
||||
jvmArgs(
|
||||
"-Dfabric.addMods=${remapJar.archiveFile.get().asFile.absolutePath}${File.pathSeparator}${remapTestmodJar.archiveFile.get().asFile.absolutePath}",
|
||||
"-Dfabric.autoTest",
|
||||
"-Dfabric.addMods=${productionMods.collect { it.absolutePath }.join(File.pathSeparator)}",
|
||||
"-Dfabric.client.gametest",
|
||||
"-Dfabric-tag-conventions-v2.missingTagTranslationWarning=fail",
|
||||
"-Dfabric-tag-conventions-v1.legacyTagWarning=fail"
|
||||
)
|
||||
|
@ -544,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")
|
||||
|
@ -553,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)
|
||||
|
@ -726,7 +732,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 {
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,175 +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.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;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.file.DirectoryStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import com.mojang.authlib.GameProfile;
|
||||
import org.spongepowered.asm.mixin.MixinEnvironment;
|
||||
|
||||
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.ReconfiguringScreen;
|
||||
import net.minecraft.client.gui.screen.TitleScreen;
|
||||
import net.minecraft.client.gui.screen.multiplayer.MultiplayerScreen;
|
||||
import net.minecraft.client.gui.screen.world.CreateWorldScreen;
|
||||
import net.minecraft.client.gui.screen.world.SelectWorldScreen;
|
||||
import net.minecraft.client.option.Perspective;
|
||||
|
||||
import net.fabricmc.api.ClientModInitializer;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
|
||||
public class FabricApiAutoTestClient implements ClientModInitializer {
|
||||
public static final boolean IS_AUTO_TEST = System.getProperty("fabric.autoTest") != null;
|
||||
|
||||
@Override
|
||||
public void onInitializeClient() {
|
||||
if (!IS_AUTO_TEST) {
|
||||
return;
|
||||
}
|
||||
|
||||
ThreadingImpl.runTestThread(this::runTest);
|
||||
}
|
||||
|
||||
private void runTest() {
|
||||
waitForLoadingComplete();
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
if (!isDirEmpty(FabricLoader.getInstance().getGameDir().resolve("saves"))) {
|
||||
waitForScreen(SelectWorldScreen.class);
|
||||
takeScreenshot("select_world_screen");
|
||||
clickScreenButton("selectWorld.create");
|
||||
}
|
||||
|
||||
{
|
||||
waitForScreen(CreateWorldScreen.class);
|
||||
clickScreenButton("selectWorld.gameMode");
|
||||
clickScreenButton("selectWorld.gameMode");
|
||||
takeScreenshot("create_world_screen");
|
||||
clickScreenButton("selectWorld.create");
|
||||
}
|
||||
|
||||
{
|
||||
// API test mods use experimental features
|
||||
waitForScreen(ConfirmScreen.class);
|
||||
clickScreenButton("gui.yes");
|
||||
}
|
||||
|
||||
{
|
||||
enableDebugHud();
|
||||
waitForWorldTicks(200);
|
||||
takeScreenshot("in_game_overworld", 0);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
{
|
||||
openInventory();
|
||||
takeScreenshot("in_game_inventory");
|
||||
closeScreen();
|
||||
}
|
||||
|
||||
{
|
||||
openGameMenu();
|
||||
takeScreenshot("game_menu");
|
||||
clickScreenButton("menu.returnToMenu");
|
||||
waitForScreen(TitleScreen.class);
|
||||
waitForServerStop();
|
||||
}
|
||||
|
||||
try (var server = new TestDedicatedServer()) {
|
||||
connectToServer(server);
|
||||
waitForWorldTicks(5);
|
||||
|
||||
final GameProfile profile = computeOnClient(MinecraftClient::getGameProfile);
|
||||
server.runCommand("op " + profile.getName());
|
||||
server.runCommand("gamemode creative " + profile.getName());
|
||||
|
||||
waitForWorldTicks(20);
|
||||
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");
|
||||
server.runCommand("debugconfig unconfig " + profile.getId());
|
||||
waitForWorldTicks(1);
|
||||
}
|
||||
|
||||
openGameMenu();
|
||||
takeScreenshot("server_game_menu");
|
||||
clickScreenButton("menu.disconnect");
|
||||
|
||||
waitForScreen(MultiplayerScreen.class);
|
||||
clickScreenButton("gui.back");
|
||||
}
|
||||
|
||||
{
|
||||
waitForScreen(TitleScreen.class);
|
||||
clickScreenButton("menu.quit");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isDirEmpty(Path path) {
|
||||
try (DirectoryStream<Path> directory = Files.newDirectoryStream(path)) {
|
||||
return !directory.iterator().hasNext();
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.test.base.client;
|
||||
|
||||
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.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.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<? extends Screen> 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<MinecraftClient, Screen> 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<MinecraftClient> predicate) {
|
||||
waitFor(what, predicate, 10 * SharedConstants.TICKS_PER_SECOND);
|
||||
}
|
||||
|
||||
private static void waitFor(String what, Predicate<MinecraftClient> 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 <E extends Throwable> void runOnClient(FailableConsumer<MinecraftClient, E> action) throws E {
|
||||
ThreadingImpl.runOnClient(() -> action.accept(MinecraftClient.getInstance()));
|
||||
}
|
||||
|
||||
public static <T, E extends Throwable> T computeOnClient(FailableFunction<MinecraftClient, T, E> action) throws E {
|
||||
MutableObject<T> result = new MutableObject<>();
|
||||
runOnClient(client -> result.setValue(action.apply(client)));
|
||||
return result.getValue();
|
||||
}
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
{
|
||||
"required": true,
|
||||
"package": "net.fabricmc.fabric.test.base.client.mixin",
|
||||
"compatibilityLevel": "JAVA_21",
|
||||
"client": [
|
||||
"CyclingButtonWidgetAccessor",
|
||||
"MinecraftClientMixin",
|
||||
"MinecraftDedicatedServerMixin",
|
||||
"ScreenAccessor",
|
||||
"TitleScreenAccessor"
|
||||
],
|
||||
"injectors": {
|
||||
"defaultRequire": 1
|
||||
},
|
||||
"mixins": [
|
||||
"MinecraftServerMixin"
|
||||
]
|
||||
}
|
5
fabric-client-gametest-api-v1/build.gradle
Normal file
5
fabric-client-gametest-api-v1/build.gradle
Normal file
|
@ -0,0 +1,5 @@
|
|||
version = getSubprojectVersion(project)
|
||||
|
||||
loom {
|
||||
accessWidenerPath = file('src/client/resources/fabric-client-gametest-api-v1.accesswidener')
|
||||
}
|
|
@ -0,0 +1,157 @@
|
|||
/*
|
||||
* 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.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;
|
||||
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. 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 in ticks for wait tasks (10 seconds).
|
||||
*/
|
||||
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<MinecraftClient> 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<MinecraftClient> 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<? extends Screen> screenClass);
|
||||
|
||||
/**
|
||||
* Opens a {@link Screen} on the client.
|
||||
*
|
||||
* @param screen The screen to open
|
||||
* @see MinecraftClient#setScreen(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
|
||||
* 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
|
||||
*/
|
||||
Path 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
|
||||
*/
|
||||
Path 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 <E> The type of checked exception that the action throws
|
||||
* @throws E When the action throws an exception
|
||||
*/
|
||||
<E extends Throwable> void runOnClient(FailableConsumer<MinecraftClient, E> 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 <T> The type of the value to return
|
||||
* @param <E> The type of the checked exception that the function throws
|
||||
* @throws E When the function throws an exception
|
||||
*/
|
||||
<T, E extends Throwable> T computeOnClient(FailableFunction<MinecraftClient, T, E> function) throws E;
|
||||
}
|
|
@ -0,0 +1,328 @@
|
|||
/*
|
||||
* 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 org.jetbrains.annotations.ApiStatus;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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. Does nothing if the key binding is already being held.
|
||||
*
|
||||
* @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. 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)
|
||||
* @see #pressKey(Function)
|
||||
* @see #holdKey(KeyBinding)
|
||||
*/
|
||||
void holdKey(Function<GameOptions, KeyBinding> keyBindingGetter);
|
||||
|
||||
/**
|
||||
* 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)
|
||||
* @see #pressKey(InputUtil.Key)
|
||||
*/
|
||||
void holdKey(InputUtil.Key key);
|
||||
|
||||
/**
|
||||
* 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)
|
||||
* @see #pressKey(int)
|
||||
*/
|
||||
void holdKey(int keyCode);
|
||||
|
||||
/**
|
||||
* 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)
|
||||
* @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. Does nothing if the key is already
|
||||
* being held.
|
||||
*
|
||||
* @see #releaseControl()
|
||||
*/
|
||||
void holdControl();
|
||||
|
||||
/**
|
||||
* Starts holding down left shift. Suitable for triggering {@link Screen#hasShiftDown()}. The key will be held until
|
||||
* it is released. Does nothing if the key is already being held.
|
||||
*
|
||||
* @see #releaseShift()
|
||||
*/
|
||||
void holdShift();
|
||||
|
||||
/**
|
||||
* Starts holding down left alt. Suitable for triggering {@link Screen#hasAltDown()}. The key will be held until it
|
||||
* 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. Does nothing if the key binding is not being held.
|
||||
*
|
||||
* @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. 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)
|
||||
* @see #releaseKey(KeyBinding)
|
||||
*/
|
||||
void releaseKey(Function<GameOptions, KeyBinding> keyBindingGetter);
|
||||
|
||||
/**
|
||||
* 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)
|
||||
*/
|
||||
void releaseKey(InputUtil.Key key);
|
||||
|
||||
/**
|
||||
* Releases a key. Does nothing if the key is not being held.
|
||||
*
|
||||
* @param keyCode The GLFW key code of the key to release
|
||||
* @see #holdKey(int)
|
||||
*/
|
||||
void releaseKey(int keyCode);
|
||||
|
||||
/**
|
||||
* Releases a mouse button. Does nothing if the mouse button is not being held.
|
||||
*
|
||||
* @param button The GLFW 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()}. Does
|
||||
* nothing if the key is not being held.
|
||||
*
|
||||
* @see #holdControl()
|
||||
*/
|
||||
void releaseControl();
|
||||
|
||||
/**
|
||||
* 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()}. Does nothing if the key is not being
|
||||
* held.
|
||||
*
|
||||
* @see #holdAlt()
|
||||
*/
|
||||
void releaseAlt();
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
/**
|
||||
* 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<GameOptions, KeyBinding> keyBindingGetter);
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
/**
|
||||
* Presses and releases a key.
|
||||
*
|
||||
* <p>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);
|
||||
|
||||
/**
|
||||
* Presses and releases a mouse button.
|
||||
*
|
||||
* @param button The GLFW 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<GameOptions, KeyBinding> 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 GLFW 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 GLFW 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.
|
||||
*
|
||||
* <p>This method is for sending Unicode text input, <em>not</em> 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);
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
|
@ -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 {@code fabric-client-gametest} entrypoint interface. See the package documentation.
|
||||
*/
|
||||
public interface FabricClientGameTest {
|
||||
/**
|
||||
* Runs the gametest.
|
||||
*/
|
||||
void runTest(ClientGameTestContext context);
|
||||
}
|
|
@ -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}.
|
||||
*
|
||||
* <h1>Lifecycle</h1>
|
||||
* 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.
|
||||
*
|
||||
* <h1>Threading</h1>
|
||||
*
|
||||
* <p>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()}.
|
||||
*
|
||||
* <p>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;
|
|
@ -0,0 +1,335 @@
|
|||
/*
|
||||
* 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.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;
|
||||
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;
|
||||
|
||||
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.client.option.CloudRenderMode;
|
||||
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<String, Object> DEFAULT_GAME_OPTIONS = new HashMap<>();
|
||||
|
||||
public static void initGameOptions(GameOptions options) {
|
||||
// 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() {
|
||||
@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> T visitObject(String key, T current, Function<String, T> decoder, Function<T, String> encoder) {
|
||||
DEFAULT_GAME_OPTIONS.put(key, current);
|
||||
return current;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> void accept(String key, SimpleOption<T> 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++) {
|
||||
ThreadingImpl.runTick();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void waitFor(Predicate<MinecraftClient> predicate) {
|
||||
ThreadingImpl.checkOnGametestThread("waitFor");
|
||||
Preconditions.checkNotNull(predicate, "predicate");
|
||||
waitFor(predicate, DEFAULT_TIMEOUT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void waitFor(Predicate<MinecraftClient> 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<? extends Screen> screenClass) {
|
||||
ThreadingImpl.checkOnGametestThread("waitForScreen");
|
||||
|
||||
if (screenClass == null) {
|
||||
waitFor(client -> client.currentScreen == null);
|
||||
} else {
|
||||
waitFor(client -> screenClass.isInstance(client.currentScreen));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setScreen(Supplier<@Nullable Screen> screen) {
|
||||
ThreadingImpl.checkOnGametestThread("setScreen");
|
||||
runOnClient(client -> client.setScreen(screen.get()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clickScreenButton(String translationKey) {
|
||||
ThreadingImpl.checkOnGametestThread("clickScreenButton");
|
||||
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) {
|
||||
ThreadingImpl.checkOnGametestThread("tryClickScreenButton");
|
||||
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) {
|
||||
MutableBoolean found = new MutableBoolean(false);
|
||||
widget.forEachChild(clickableWidget -> {
|
||||
if (!found.booleanValue()) {
|
||||
found.setValue(pressMatchingButton(clickableWidget, buttonText));
|
||||
}
|
||||
});
|
||||
|
||||
if (found.booleanValue()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 Path takeScreenshot(String name) {
|
||||
ThreadingImpl.checkOnGametestThread("takeScreenshot");
|
||||
Preconditions.checkNotNull(name, "name");
|
||||
return takeScreenshot(name, 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Path 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) -> {
|
||||
});
|
||||
});
|
||||
|
||||
return FabricLoader.getInstance().getGameDir().resolve("screenshots").resolve(name + ".png");
|
||||
}
|
||||
|
||||
@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> T visitObject(String key, T current, Function<String, T> decoder, Function<T, String> encoder) {
|
||||
return (T) DEFAULT_GAME_OPTIONS.get(key);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public <T> void accept(String key, SimpleOption<T> option) {
|
||||
option.setValue((T) DEFAULT_GAME_OPTIONS.get(key));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public <E extends Throwable> void runOnClient(FailableConsumer<MinecraftClient, E> action) throws E {
|
||||
ThreadingImpl.checkOnGametestThread("runOnClient");
|
||||
Preconditions.checkNotNull(action, "action");
|
||||
|
||||
ThreadingImpl.runOnClient(() -> action.accept(MinecraftClient.getInstance()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T, E extends Throwable> T computeOnClient(FailableFunction<MinecraftClient, T, E> function) throws E {
|
||||
ThreadingImpl.checkOnGametestThread("computeOnClient");
|
||||
Preconditions.checkNotNull(function, "function");
|
||||
|
||||
MutableObject<T> result = new MutableObject<>();
|
||||
ThreadingImpl.runOnClient(() -> result.setValue(function.apply(MinecraftClient.getInstance())));
|
||||
return result.getValue();
|
||||
}
|
||||
}
|
|
@ -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<InputUtil.Key> 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 holdKey(KeyBinding keyBinding) {
|
||||
ThreadingImpl.checkOnGametestThread("holdKey");
|
||||
Preconditions.checkNotNull(keyBinding, "keyBinding");
|
||||
|
||||
holdKey(getBoundKey(keyBinding, "hold"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void holdKey(Function<GameOptions, KeyBinding> keyBindingGetter) {
|
||||
ThreadingImpl.checkOnGametestThread("holdKey");
|
||||
Preconditions.checkNotNull(keyBindingGetter, "keyBindingGetter");
|
||||
|
||||
KeyBinding keyBinding = context.computeOnClient(client -> keyBindingGetter.apply(client.options));
|
||||
holdKey(keyBinding);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void holdKey(InputUtil.Key key) {
|
||||
ThreadingImpl.checkOnGametestThread("holdKey");
|
||||
Preconditions.checkNotNull(key, "key");
|
||||
|
||||
if (KEYS_DOWN.add(key)) {
|
||||
context.runOnClient(client -> pressOrReleaseKey(client, key, GLFW.GLFW_PRESS));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void holdKey(int keyCode) {
|
||||
ThreadingImpl.checkOnGametestThread("holdKey");
|
||||
|
||||
holdKey(InputUtil.Type.KEYSYM.createFromCode(keyCode));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void holdMouse(int button) {
|
||||
ThreadingImpl.checkOnGametestThread("holdMouse");
|
||||
|
||||
holdKey(InputUtil.Type.MOUSE.createFromCode(button));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void holdControl() {
|
||||
ThreadingImpl.checkOnGametestThread("holdControl");
|
||||
|
||||
holdKey(MinecraftClient.IS_SYSTEM_MAC ? InputUtil.GLFW_KEY_LEFT_SUPER : InputUtil.GLFW_KEY_LEFT_CONTROL);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void holdShift() {
|
||||
ThreadingImpl.checkOnGametestThread("holdShift");
|
||||
|
||||
holdKey(InputUtil.GLFW_KEY_LEFT_SHIFT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void holdAlt() {
|
||||
ThreadingImpl.checkOnGametestThread("holdAlt");
|
||||
|
||||
holdKey(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<GameOptions, KeyBinding> 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 pressKey(KeyBinding keyBinding) {
|
||||
ThreadingImpl.checkOnGametestThread("pressKey");
|
||||
Preconditions.checkNotNull(keyBinding, "keyBinding");
|
||||
|
||||
pressKey(getBoundKey(keyBinding, "press"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pressKey(Function<GameOptions, KeyBinding> 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");
|
||||
|
||||
holdKey(key);
|
||||
releaseKey(key);
|
||||
}
|
||||
|
||||
@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 holdKeyFor(KeyBinding keyBinding, int ticks) {
|
||||
ThreadingImpl.checkOnGametestThread("holdKeyFor");
|
||||
Preconditions.checkNotNull(keyBinding, "keyBinding");
|
||||
Preconditions.checkArgument(ticks > 0, "ticks must be positive");
|
||||
|
||||
holdKeyFor(getBoundKey(keyBinding, "hold"), ticks);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void holdKeyFor(Function<GameOptions, KeyBinding> 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));
|
||||
holdKeyFor(keyBinding, ticks);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void holdKeyFor(InputUtil.Key key, int ticks) {
|
||||
ThreadingImpl.checkOnGametestThread("holdKeyFor");
|
||||
Preconditions.checkNotNull(key, "key");
|
||||
Preconditions.checkArgument(ticks > 0, "ticks must be positive");
|
||||
|
||||
holdKey(key);
|
||||
context.waitTicks(ticks);
|
||||
releaseKey(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void holdKeyFor(int keyCode, int ticks) {
|
||||
ThreadingImpl.checkOnGametestThread("holdKeyFor");
|
||||
Preconditions.checkArgument(ticks > 0, "ticks must be positive");
|
||||
|
||||
holdKeyFor(InputUtil.Type.KEYSYM.createFromCode(keyCode), ticks);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void holdMouseFor(int button, int ticks) {
|
||||
ThreadingImpl.checkOnGametestThread("holdMouseFor");
|
||||
Preconditions.checkArgument(ticks > 0, "ticks must be positive");
|
||||
|
||||
holdKeyFor(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't bound to a key".formatted(action, keyBinding.getTranslationKey()));
|
||||
}
|
||||
|
||||
return boundKey;
|
||||
}
|
||||
}
|
|
@ -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.client.gametest;
|
||||
|
||||
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<String> myTargets, Set<String> otherTargets) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> 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) {
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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 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 String ENTRYPOINT_KEY = "fabric-client-gametest";
|
||||
|
||||
public static void start() {
|
||||
// make the game think the window is focused
|
||||
MinecraftClient.getInstance().onWindowFocusChanged(true);
|
||||
|
||||
List<FabricClientGameTest> gameTests = FabricLoader.getInstance().getEntrypoints(ENTRYPOINT_KEY, FabricClientGameTest.class);
|
||||
|
||||
ThreadingImpl.runTestThread(() -> {
|
||||
ClientGameTestContextImpl context = new ClientGameTestContextImpl();
|
||||
|
||||
for (FabricClientGameTest gameTest : gameTests) {
|
||||
context.restoreDefaultGameOptions();
|
||||
|
||||
try {
|
||||
gameTest.runTest(context);
|
||||
} finally {
|
||||
context.getInput().clearKeysDown();
|
||||
checkFinalGameTestState(context, gameTest.getClass().getName());
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
if (!(client.currentScreen instanceof TitleScreen)) {
|
||||
throw new AssertionError("Client gametest %s did not finish on the title screen".formatted(testClassName));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.fabricmc.fabric.test.base.client;
|
||||
package net.fabricmc.fabric.impl.client.gametest;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.fabricmc.fabric.test.base.client;
|
||||
package net.fabricmc.fabric.impl.client.gametest;
|
||||
|
||||
import java.util.concurrent.Phaser;
|
||||
import java.util.concurrent.Semaphore;
|
||||
|
@ -23,6 +23,8 @@ import com.google.common.base.Preconditions;
|
|||
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;
|
||||
|
||||
/**
|
||||
* <h1>Implementation notes</h1>
|
||||
|
@ -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 final class ThreadingImpl {
|
|||
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 final class ThreadingImpl {
|
|||
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 <E extends Throwable> void runOnClient(FailableRunnable<E> 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<E> thrown = new MutableObject<>();
|
||||
|
@ -159,7 +167,7 @@ public final class ThreadingImpl {
|
|||
@SuppressWarnings("unchecked")
|
||||
public static <E extends Throwable> void runOnServer(FailableRunnable<E> 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<E> thrown = new MutableObject<>();
|
||||
|
@ -188,7 +196,7 @@ public final class ThreadingImpl {
|
|||
}
|
||||
|
||||
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();
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.fabricmc.fabric.test.base.client.mixin;
|
||||
package net.fabricmc.fabric.mixin.client.gametest;
|
||||
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.gen.Accessor;
|
|
@ -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);
|
||||
}
|
|
@ -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 = "<init>", at = @At("RETURN"))
|
||||
private void onCreateGameOptions(CallbackInfo ci) {
|
||||
ClientGameTestContextImpl.initGameOptions((GameOptions) (Object) this);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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;
|
||||
|
||||
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 useGameTestInputForKeyPressed(long window, int keyCode, CallbackInfoReturnable<Boolean> cir) {
|
||||
cir.setReturnValue(ClientGameTestInputImpl.isKeyDown(keyCode));
|
||||
}
|
||||
|
||||
@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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -14,12 +14,14 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.fabricmc.fabric.test.base.client.mixin;
|
||||
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,86 +29,93 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
|||
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.test.base.client.FabricApiAutoTestClient;
|
||||
import net.fabricmc.fabric.test.base.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<Void> 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 = "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) {
|
||||
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);
|
||||
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.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;
|
||||
}
|
||||
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 +124,14 @@ public class MinecraftClientMixin {
|
|||
|
||||
@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 +140,16 @@ public class MinecraftClientMixin {
|
|||
|
||||
@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<MinecraftClient> 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");
|
||||
}
|
||||
Preconditions.checkState(
|
||||
Thread.currentThread() != ThreadingImpl.testThread,
|
||||
"MinecraftClient.getInstance() cannot be called from the gametest thread. Try using ClientGameTestContext.runOnClient or ClientGameTestContext.computeOnClient"
|
||||
);
|
||||
}
|
||||
}
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.fabricmc.fabric.test.base.client.mixin;
|
||||
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 org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
|||
|
||||
import net.minecraft.server.dedicated.MinecraftDedicatedServer;
|
||||
|
||||
import net.fabricmc.fabric.test.base.client.TestDedicatedServer;
|
||||
import net.fabricmc.fabric.impl.client.gametest.TestDedicatedServer;
|
||||
|
||||
@Mixin(MinecraftDedicatedServer.class)
|
||||
public abstract class MinecraftDedicatedServerMixin {
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.fabricmc.fabric.test.base.client.mixin;
|
||||
package net.fabricmc.fabric.mixin.client.gametest;
|
||||
|
||||
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
|
||||
|
@ -25,66 +25,57 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
|||
|
||||
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.client.gametest.ThreadingImpl;
|
||||
|
||||
@Mixin(MinecraftServer.class)
|
||||
public class MinecraftServerMixin {
|
||||
@WrapMethod(method = "runServer")
|
||||
private void onRunServer(Operation<Void> 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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.fabricmc.fabric.test.base.client.mixin;
|
||||
package net.fabricmc.fabric.mixin.client.gametest;
|
||||
|
||||
import java.util.List;
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
Binary file not shown.
After ![]() (image error) Size: 1.5 KiB |
|
@ -0,0 +1,2 @@
|
|||
accessWidener v2 named
|
||||
accessible class net/minecraft/client/option/GameOptions$Visitor
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"required": true,
|
||||
"package": "net.fabricmc.fabric.mixin.client.gametest",
|
||||
"compatibilityLevel": "JAVA_21",
|
||||
"mixins": [
|
||||
"CyclingButtonWidgetAccessor",
|
||||
"GameOptionsAccessor",
|
||||
"GameOptionsMixin",
|
||||
"InputUtilMixin",
|
||||
"KeyBindingAccessor",
|
||||
"KeyboardAccessor",
|
||||
"MinecraftClientMixin",
|
||||
"MinecraftDedicatedServerMixin",
|
||||
"MinecraftServerMixin",
|
||||
"MouseAccessor",
|
||||
"ScreenAccessor",
|
||||
"WindowMixin"
|
||||
],
|
||||
"plugin": "net.fabricmc.fabric.impl.client.gametest.ClientGameTestMixinConfigPlugin",
|
||||
"injectors": {
|
||||
"defaultRequire": 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"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"
|
||||
],
|
||||
"accessWidener": "fabric-client-gametest-api-v1.accesswidener",
|
||||
"custom": {
|
||||
"fabric-api:module-lifecycle": "experimental"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,192 @@
|
|||
/*
|
||||
* 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.test.client.gametest;
|
||||
|
||||
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.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.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");
|
||||
}
|
||||
|
||||
if (!isDirEmpty(FabricLoader.getInstance().getGameDir().resolve("saves"))) {
|
||||
context.waitForScreen(SelectWorldScreen.class);
|
||||
context.takeScreenshot("select_world_screen");
|
||||
context.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
|
||||
context.waitForScreen(ConfirmScreen.class);
|
||||
context.clickScreenButton("gui.yes");
|
||||
}
|
||||
|
||||
{
|
||||
enableDebugHud(context);
|
||||
waitForWorldTicks(context, 200);
|
||||
context.takeScreenshot("in_game_overworld", 0);
|
||||
}
|
||||
|
||||
{
|
||||
context.getInput().pressKey(options -> options.chatKey);
|
||||
context.waitTick();
|
||||
context.getInput().typeChars("Hello, World!");
|
||||
context.getInput().pressKey(InputUtil.GLFW_KEY_ENTER);
|
||||
context.takeScreenshot("chat_message_sent", 5);
|
||||
}
|
||||
|
||||
MixinEnvironment.getCurrentEnvironment().audit();
|
||||
|
||||
{
|
||||
// See if the player render events are working.
|
||||
setPerspective(context, Perspective.THIRD_PERSON_BACK);
|
||||
context.takeScreenshot("in_game_overworld_third_person");
|
||||
setPerspective(context, Perspective.FIRST_PERSON);
|
||||
}
|
||||
|
||||
{
|
||||
context.getInput().pressKey(options -> options.inventoryKey);
|
||||
context.takeScreenshot("in_game_inventory");
|
||||
context.setScreen(() -> null);
|
||||
}
|
||||
|
||||
{
|
||||
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(context, server);
|
||||
waitForWorldTicks(context, 5);
|
||||
|
||||
final GameProfile profile = context.computeOnClient(MinecraftClient::getGameProfile);
|
||||
server.runCommand("op " + profile.getName());
|
||||
server.runCommand("gamemode creative " + profile.getName());
|
||||
|
||||
waitForWorldTicks(context, 20);
|
||||
context.takeScreenshot("server_in_game", 0);
|
||||
|
||||
{ // Test that we can enter and exit configuration
|
||||
server.runCommand("debugconfig config " + profile.getName());
|
||||
context.waitForScreen(ReconfiguringScreen.class);
|
||||
context.takeScreenshot("server_config");
|
||||
server.runCommand("debugconfig unconfig " + profile.getId());
|
||||
waitForWorldTicks(context, 1);
|
||||
}
|
||||
|
||||
context.setScreen(() -> new GameMenuScreen(true));
|
||||
context.takeScreenshot("server_game_menu");
|
||||
context.clickScreenButton("menu.disconnect");
|
||||
|
||||
context.waitForScreen(MultiplayerScreen.class);
|
||||
context.clickScreenButton("gui.back");
|
||||
}
|
||||
|
||||
{
|
||||
context.waitForScreen(TitleScreen.class);
|
||||
context.clickScreenButton("menu.quit");
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isDirEmpty(Path path) {
|
||||
try (DirectoryStream<Path> directory = Files.newDirectoryStream(path)) {
|
||||
return !directory.iterator().hasNext();
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.fabricmc.fabric.test.base.client.mixin;
|
||||
package net.fabricmc.fabric.test.client.gametest.mixin;
|
||||
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.gen.Accessor;
|
|
@ -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"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"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": {
|
||||
"fabric-client-gametest": [
|
||||
"net.fabricmc.fabric.test.client.gametest.ClientGameTestTest"
|
||||
]
|
||||
},
|
||||
"mixins": [
|
||||
"fabric-client-gametest-api-v1-test.mixins.json"
|
||||
]
|
||||
}
|
|
@ -17,6 +17,7 @@ fabric-biome-api-v1-version=15.0.5
|
|||
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
|
||||
|
|
|
@ -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'
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue