diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index f92e6d7730..90cd7e6690 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -43,6 +43,7 @@ jobs:
with:
distribution: 'microsoft'
java-version: '21'
+ - run: mkdir run && echo "eula=true" >> run/eula.txt
- name: Run Client Gametests
uses: modmuss50/xvfb-action@v1
with:
diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/ClientGameTestContext.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/ClientGameTestContext.java
index 9c46076601..5c943fff89 100644
--- a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/ClientGameTestContext.java
+++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/ClientGameTestContext.java
@@ -30,8 +30,9 @@
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.
+ * 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 {
@@ -61,8 +62,9 @@ public interface ClientGameTestContext {
* 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
+ * @return The number of ticks waited
*/
- void waitFor(Predicate predicate);
+ int waitFor(Predicate predicate);
/**
* Waits for a predicate to be true. Fails if the predicate is not satisfied after {@code timeout} ticks. If
@@ -70,16 +72,18 @@ public interface ClientGameTestContext {
*
* @param predicate The predicate to check
* @param timeout The number of ticks before timing out
+ * @return The number of ticks waited
*/
- void waitFor(Predicate predicate, int timeout);
+ int waitFor(Predicate predicate, int timeout);
/**
* Waits for the given screen class to be shown. If {@code screenClass} is {@code null}, waits for the current
* screen to be {@code null}. Fails if the screen does not open after {@link #DEFAULT_TIMEOUT} ticks.
*
* @param screenClass The screen class to wait to open
+ * @return The number of ticks waited
*/
- void waitForScreen(@Nullable Class extends Screen> screenClass);
+ int waitForScreen(@Nullable Class extends Screen> screenClass);
/**
* Opens a {@link Screen} on the client.
@@ -126,7 +130,14 @@ public interface ClientGameTestContext {
*
* @return The client gametest input handler
*/
- ClientGameTestInput getInput();
+ TestInput getInput();
+
+ /**
+ * Creates a world builder for creating singleplayer worlds and dedicated servers.
+ *
+ * @return A new world builder
+ */
+ TestWorldBuilder worldBuilder();
/**
* Restores all game options in {@link MinecraftClient#options} to their default values for client gametests. This
diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/TestClientWorldContext.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/TestClientWorldContext.java
new file mode 100644
index 0000000000..8f1fca27e1
--- /dev/null
+++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/TestClientWorldContext.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.api.client.gametest.v1;
+
+import org.jetbrains.annotations.ApiStatus;
+
+import net.minecraft.SharedConstants;
+import net.minecraft.client.world.ClientWorld;
+import net.minecraft.util.math.BlockPos;
+
+/**
+ * Context for a client gametest containing various helpful functions while a client world is open.
+ *
+ * Functions in this class can only be called on the client gametest thread.
+ */
+@ApiStatus.NonExtendable
+public interface TestClientWorldContext {
+ /**
+ * The default timeout in ticks to wait for chunks to load/render (1 minute).
+ */
+ int DEFAULT_CHUNK_LOAD_TIMEOUT = SharedConstants.TICKS_PER_MINUTE;
+
+ /**
+ * Waits for all chunks that will be downloaded from the server to be downloaded. Fails if the chunks haven't been
+ * downloaded after {@link #DEFAULT_CHUNK_LOAD_TIMEOUT} ticks. See {@link #waitForChunksDownload(int)} for details.
+ *
+ * @return The number of ticks waited
+ */
+ default int waitForChunksDownload() {
+ return waitForChunksDownload(DEFAULT_CHUNK_LOAD_TIMEOUT);
+ }
+
+ /**
+ Waits for all chunks that will be downloaded from the server to be downloaded. After this, methods such as
+ * {@link ClientWorld#getChunk(int, int)} and {@link ClientWorld#getBlockState(BlockPos)} will return the expected
+ * value. However, the chunks may not yet be rendered and may not appear in screenshots, if you need this, use
+ * {@link #waitForChunksRender(int)} instead. Fails if the chunks haven't been downloaded after {@code timeout}
+ * ticks.
+ *
+ * @param timeout The number of ticks before timing out
+ * @return The number of ticks waited
+ */
+ int waitForChunksDownload(int timeout);
+
+ /**
+ * Waits for all chunks to be downloaded and rendered. After this, all chunks that will ever be visible are visible
+ * in screenshots. Fails if the chunks haven't been downloaded and rendered after
+ * {@link #DEFAULT_CHUNK_LOAD_TIMEOUT} ticks.
+ *
+ * @return The number of ticks waited
+ */
+ default int waitForChunksRender() {
+ return waitForChunksRender(DEFAULT_CHUNK_LOAD_TIMEOUT);
+ }
+
+ /**
+ * Waits for all chunks to be downloaded and rendered. After this, all chunks that will ever be visible are visible
+ * in screenshots. Fails if the chunks haven't been downloaded and rendered after {@code timeout} ticks.
+ *
+ * @param timeout The number of ticks before timing out
+ * @return The number of ticks waited
+ */
+ default int waitForChunksRender(int timeout) {
+ return waitForChunksRender(true, timeout);
+ }
+
+ /**
+ * Waits for all chunks to be rendered, optionally waiting for chunks to be downloaded first. After this, all chunks
+ * that are present in the client world will be visible in screenshots. Fails if the chunks haven't been rendered
+ * (and optionally downloaded) after {@link #DEFAULT_CHUNK_LOAD_TIMEOUT} ticks.
+ *
+ * @param waitForDownload Whether to wait for chunks to be downloaded
+ * @return The number of ticks waited
+ */
+ default int waitForChunksRender(boolean waitForDownload) {
+ return waitForChunksRender(waitForDownload, DEFAULT_CHUNK_LOAD_TIMEOUT);
+ }
+
+ /**
+ * Waits for all chunks to be rendered, optionally waiting for chunks to be downloaded first. After this, all chunks
+ * that are present in the client world will be visible in screenshots. Fails if the chunks haven't been rendered
+ * (and optionally downloaded) after {@code timeout} ticks.
+ *
+ * @param waitForDownload Whether to wait for chunks to be downloaded
+ * @param timeout The number of ticks before timing out
+ * @return The number of ticks waited
+ */
+ int waitForChunksRender(boolean waitForDownload, int timeout);
+}
diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/TestDedicatedServerContext.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/TestDedicatedServerContext.java
new file mode 100644
index 0000000000..a9f0f6a3f6
--- /dev/null
+++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/TestDedicatedServerContext.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.api.client.gametest.v1;
+
+import org.jetbrains.annotations.ApiStatus;
+
+/**
+ * Context for a client gametest containing various helpful functions while an in-process dedicated server is running.
+ * This class implements {@link AutoCloseable} and is intended to be used in a try-with-resources statement. When
+ * closed, the dedicated server will be stopped.
+ *
+ *
Functions in this class can only be called on the client gametest thread.
+ */
+@ApiStatus.NonExtendable
+public interface TestDedicatedServerContext extends TestServerContext, AutoCloseable {
+ /**
+ * Connects the client to the dedicated server. The resulting connection is intended to be used in a
+ * try-with-resources statement.
+ *
+ * @return The connection handle to the dedicated server
+ */
+ TestServerConnection connect();
+
+ /**
+ * Stops the dedicated server.
+ */
+ @Override
+ void close();
+}
diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/ClientGameTestInput.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/TestInput.java
similarity index 99%
rename from fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/ClientGameTestInput.java
rename to fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/TestInput.java
index 5bbf839965..7da281777d 100644
--- a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/ClientGameTestInput.java
+++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/TestInput.java
@@ -29,7 +29,7 @@
* The client gametest input handler used to simulate inputs to the client.
*/
@ApiStatus.NonExtendable
-public interface ClientGameTestInput {
+public interface TestInput {
/**
* 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.
diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/TestServerConnection.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/TestServerConnection.java
new file mode 100644
index 0000000000..88e7b58b3e
--- /dev/null
+++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/TestServerConnection.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.api.client.gametest.v1;
+
+import org.jetbrains.annotations.ApiStatus;
+
+/**
+ * Context for a connection to a dedicated server containing various helpful functions while the connection is alive.
+ * This class implements {@link AutoCloseable} and is intended to be used in a try-with-resources statement. When
+ * closed, the client will be disconnected from the server.
+ *
+ *
Functions in this class can only be called on the client gametest thread.
+ */
+@ApiStatus.NonExtendable
+public interface TestServerConnection extends AutoCloseable {
+ /**
+ * Gets the client world context for this connection.
+ *
+ * @return The client world context
+ */
+ TestClientWorldContext getClientWorld();
+
+ /**
+ * Disconnects the client from the dedicated server.
+ */
+ @Override
+ void close();
+}
diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/TestServerContext.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/TestServerContext.java
new file mode 100644
index 0000000000..41cccbb206
--- /dev/null
+++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/TestServerContext.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.api.client.gametest.v1;
+
+import org.apache.commons.lang3.function.FailableConsumer;
+import org.apache.commons.lang3.function.FailableFunction;
+import org.jetbrains.annotations.ApiStatus;
+
+import net.minecraft.server.MinecraftServer;
+
+/**
+ * Context for a client gametest containing various helpful functions while a server (integrated or dedicated) is
+ * running.
+ *
+ *
Functions in this class can only be called on the client gametest thread.
+ */
+@ApiStatus.NonExtendable
+public interface TestServerContext {
+ /**
+ * Runs a command on the server.
+ *
+ * @param command The command to run
+ */
+ void runCommand(String command);
+
+ /**
+ * Runs the given action on the server thread, and waits for it to complete.
+ *
+ * @param action The action to run on the server thread
+ * @param The type of the checked exception that the action throws
+ * @throws E When the action throws an exception
+ */
+ void runOnServer(FailableConsumer action) throws E;
+
+ /**
+ * Runs the given function on the server thread, and returns the result.
+ *
+ * @param function The function to run on the server thread
+ * @return The result of the function
+ * @param The type of the value to return
+ * @param The type of the checked exception that the function throws
+ * @throws E When the function throws an exception
+ */
+ T computeOnServer(FailableFunction function) throws E;
+}
diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/TestSingleplayerContext.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/TestSingleplayerContext.java
new file mode 100644
index 0000000000..c51f5ba161
--- /dev/null
+++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/TestSingleplayerContext.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.api.client.gametest.v1;
+
+import org.jetbrains.annotations.ApiStatus;
+
+/**
+ * Context for a client gametest containing various helpful functions while a singleplayer game is open.
+ *
+ * Functions in this class can only be called on the client gametest thread.
+ */
+@ApiStatus.NonExtendable
+public interface TestSingleplayerContext extends AutoCloseable {
+ /**
+ * Gets the handle for the world save.
+ *
+ * @return The handle for the world save
+ */
+ TestWorldSave getWorldSave();
+
+ /**
+ * Gets the handle for the client world.
+ *
+ * @return The handle for the client world
+ */
+ TestClientWorldContext getClientWorld();
+
+ /**
+ * Gets the handle for the integrated server.
+ *
+ * @return The handle for the integrated server
+ */
+ TestServerContext getServer();
+
+ /**
+ * Closes the singleplayer world.
+ */
+ @Override
+ void close();
+}
diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/TestWorldBuilder.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/TestWorldBuilder.java
new file mode 100644
index 0000000000..d669fcf6bb
--- /dev/null
+++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/TestWorldBuilder.java
@@ -0,0 +1,79 @@
+/*
+ * 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.Properties;
+import java.util.function.Consumer;
+
+import org.jetbrains.annotations.ApiStatus;
+
+import net.minecraft.client.gui.screen.world.WorldCreator;
+
+/**
+ * A builder used for creating singleplayer worlds and dedicated servers.
+ *
+ *
Worlds from this builder default to being flat worlds with settings and game rules designed for consistency of
+ * tests, see the package documentation for details. To disable this, use {@link #setUseConsistentSettings}. If you need
+ * to re-enable a particular setting, you can override it using {@link #adjustSettings}.
+ */
+@ApiStatus.NonExtendable
+public interface TestWorldBuilder {
+ /**
+ * Sets whether to use consistent world settings. Consistent settings are designed for consistency of tests. See the
+ * package documentation for details on what the consistent settings are.
+ *
+ *
If disabled, the world builder will default to creating worlds with the default world preset in survival mode,
+ * as if clicking straight through the create world screen without changing any settings.
+ *
+ * @param useConsistentSettings Whether to use consistent settings
+ * @return This world builder instance
+ */
+ TestWorldBuilder setUseConsistentSettings(boolean useConsistentSettings);
+
+ /**
+ * Adjusts the world settings from the default. Can be used to adjust anything that can be changed in the create
+ * world screen, including generation settings and game rules.
+ *
+ * @param settingsAdjuster The function to adjust the world settings
+ * @return This world builder instance
+ */
+ TestWorldBuilder adjustSettings(Consumer settingsAdjuster);
+
+ /**
+ * Creates and joins a singleplayer world with the configured world settings.
+ *
+ * @return The singleplayer context of the world that was joined
+ */
+ TestSingleplayerContext create();
+
+ /**
+ * Creates and starts a dedicated server with the configured world settings.
+ *
+ * @return The dedicated server context of the server that was created
+ */
+ default TestDedicatedServerContext createServer() {
+ return createServer(new Properties());
+ }
+
+ /**
+ * Creates and starts a dedicated server with the configured world settings and some custom server properties.
+ *
+ * @param serverProperties The custom server properties to be written to the {@code server.properties} file.
+ * @return The dedicated server context of the server that was created.
+ */
+ TestDedicatedServerContext createServer(Properties serverProperties);
+}
diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/TestWorldSave.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/TestWorldSave.java
new file mode 100644
index 0000000000..2287fd69f1
--- /dev/null
+++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/TestWorldSave.java
@@ -0,0 +1,42 @@
+/*
+ * 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 org.jetbrains.annotations.ApiStatus;
+
+/**
+ * A handle for a singleplayer world save. Can be used to reopen a singleplayer world that was created earlier in the
+ * same gametest.
+ */
+@ApiStatus.NonExtendable
+public interface TestWorldSave {
+ /**
+ * Gets the directory of the world save.
+ *
+ * @return The world save directory
+ */
+ Path getSaveDirectory();
+
+ /**
+ * Opens and joins the singleplayer world.
+ *
+ * @return The singleplayer context of the world that was joined
+ */
+ TestSingleplayerContext open();
+}
diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/package-info.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/package-info.java
index 521dd389e4..e8617bdb34 100644
--- a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/package-info.java
+++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/package-info.java
@@ -21,6 +21,138 @@
* 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.
+ *
+ *
Default settings
+ * The client gametest API adjusts some default settings, usually for consistency of tests. These settings can always be
+ * changed back to the default value or a different value inside a gametest.
+ *
+ * Game options
+ *
+ *
+ * Setting name |
+ * Gametest default |
+ * Vanilla default |
+ * Reason |
+ *
+ *
+ * {@linkplain net.minecraft.client.option.GameOptions#tutorialStep Tutorial step} |
+ * {@link net.minecraft.client.tutorial.TutorialStep#NONE NONE} |
+ * {@link net.minecraft.client.tutorial.TutorialStep#MOVEMENT MOVEMENT} |
+ * Consistency of tests |
+ *
+ *
+ * {@linkplain net.minecraft.client.option.GameOptions#getCloudRenderMode() Cloud render mode} |
+ * {@link net.minecraft.client.option.CloudRenderMode#OFF OFF} |
+ * {@link net.minecraft.client.option.CloudRenderMode#FANCY FANCY} |
+ * Consistency of tests |
+ *
+ *
+ * {@linkplain net.minecraft.client.option.GameOptions#onboardAccessibility Onboard accessibility} |
+ * {@code false} |
+ * {@code true} |
+ * Would cause the game test runner to have to click through the onboard accessibility prompt |
+ *
+ *
+ * {@linkplain net.minecraft.client.option.GameOptions#getViewDistance() View distance} |
+ * {@code 5} |
+ * {@code 10} |
+ * Speeds up loading of chunks, especially for functions such as
+ * {@link net.fabricmc.fabric.api.client.gametest.v1.TestClientWorldContext#waitForChunksRender() TestClientWorldContext.waitForChunksRender()} |
+ *
+ *
+ * {@linkplain net.minecraft.client.option.GameOptions#getSoundVolumeOption(net.minecraft.sound.SoundCategory) Music volume} |
+ * {@code 0.0} |
+ * {@code 1.0} |
+ * The game music is annoying while running gametests |
+ *
+ *
+ *
+ * World creation options
+ * These adjusted defaults only apply if the world builder's
+ * {@linkplain net.fabricmc.fabric.api.client.gametest.v1.TestWorldBuilder#setUseConsistentSettings(boolean) consistent settings}
+ * have not been set to {@code false}.
+ *
+ *
+ *
+ * Setting name |
+ * Gametest default |
+ * Vanilla default |
+ * Reason |
+ *
+ *
+ * {@linkplain net.minecraft.client.gui.screen.world.WorldCreator#setWorldType(net.minecraft.client.gui.screen.world.WorldCreator.WorldType) World type} |
+ * {@link net.minecraft.world.gen.WorldPresets#FLAT FLAT} |
+ * {@link net.minecraft.world.gen.WorldPresets#DEFAULT DEFAULT} |
+ * Creates cleaner test cases |
+ *
+ *
+ * {@linkplain net.minecraft.client.gui.screen.world.WorldCreator#setSeed(String) Seed} |
+ * {@code 1} |
+ * Random value |
+ * Consistency of tests |
+ *
+ *
+ * {@linkplain net.minecraft.client.gui.screen.world.WorldCreator#setGenerateStructures(boolean) Generate structures} |
+ * {@code false} |
+ * {@code true} |
+ * Consistency of tests and creates cleaner tests |
+ *
+ *
+ * {@linkplain net.minecraft.world.GameRules#DO_DAYLIGHT_CYCLE Do daylight cycle} |
+ * {@code false} |
+ * {@code true} |
+ * Consistency of tests |
+ *
+ *
+ * {@linkplain net.minecraft.world.GameRules#DO_WEATHER_CYCLE Do weather cycle} |
+ * {@code false} |
+ * {@code true} |
+ * Consistency of tests |
+ *
+ *
+ * {@linkplain net.minecraft.world.GameRules#DO_MOB_SPAWNING Do mob spawning} |
+ * {@code false} |
+ * {@code true} |
+ * Consistency of tests |
+ *
+ *
+ *
+ * Dedicated server properties
+ *
+ *
+ * Setting name |
+ * Gametest default |
+ * Vanilla default |
+ * Reason |
+ *
+ *
+ * {@code online-mode} |
+ * {@code false} |
+ * {@code true} |
+ * Allows the gametest client to connect to the dedicated server without being logged in to a Minecraft
+ * account |
+ *
+ *
+ * {@code sync-chunk-writes} |
+ * {@code true} on Windows, {@code false} on other operating systems |
+ * {@code true} |
+ * Causes world saving and closing to be extremely slow (on the order of many seconds to minutes) on Unix
+ * systems. The vanilla default is set correctly in singleplayer but not on dedicated servers. |
+ *
+ *
+ * {@code spawn-protection} |
+ * {@code 0} |
+ * {@code 16} |
+ * Spawn protection prevents non-opped players from modifying the world within a certain radius of the world
+ * spawn point, a likely source of confusion when writing gametests |
+ *
+ *
+ * {@code max-players} |
+ * {@code 1} |
+ * {@code 20} |
+ * Stops other players from joining the server and interfering with the test |
+ *
+ *
*/
@ApiStatus.Experimental
package net.fabricmc.fabric.api.client.gametest.v1;
diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/ClientGameTestContextImpl.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/ClientGameTestContextImpl.java
index 3674b90b6a..04e0b5a1b6 100644
--- a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/ClientGameTestContextImpl.java
+++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/ClientGameTestContextImpl.java
@@ -48,13 +48,14 @@
import net.minecraft.util.Nullables;
import net.fabricmc.fabric.api.client.gametest.v1.ClientGameTestContext;
+import net.fabricmc.fabric.api.client.gametest.v1.TestWorldBuilder;
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 final TestInputImpl input = new TestInputImpl(this);
private static final Map DEFAULT_GAME_OPTIONS = new HashMap<>();
@@ -66,6 +67,9 @@ public static void initGameOptions(GameOptions options) {
// Messes with game tests starting
options.onboardAccessibility = false;
+ // Makes chunk rendering finish sooner
+ options.getViewDistance().setValue(5);
+
// Just annoying
options.getSoundVolumeOption(SoundCategory.MUSIC).setValue(0.0);
@@ -124,27 +128,32 @@ public void waitTicks(int ticks) {
}
@Override
- public void waitFor(Predicate predicate) {
+ public int waitFor(Predicate predicate) {
ThreadingImpl.checkOnGametestThread("waitFor");
Preconditions.checkNotNull(predicate, "predicate");
- waitFor(predicate, DEFAULT_TIMEOUT);
+ return waitFor(predicate, DEFAULT_TIMEOUT);
}
@Override
- public void waitFor(Predicate predicate, int timeout) {
+ public int waitFor(Predicate predicate, int timeout) {
ThreadingImpl.checkOnGametestThread("waitFor");
Preconditions.checkNotNull(predicate, "predicate");
if (timeout == NO_TIMEOUT) {
+ int ticksWaited = 0;
+
while (!computeOnClient(predicate::test)) {
+ ticksWaited++;
ThreadingImpl.runTick();
}
+
+ return ticksWaited;
} else {
Preconditions.checkArgument(timeout > 0, "timeout must be positive");
for (int i = 0; i < timeout; i++) {
if (computeOnClient(predicate::test)) {
- return;
+ return i;
}
ThreadingImpl.runTick();
@@ -153,17 +162,19 @@ public void waitFor(Predicate predicate, int timeout) {
if (!computeOnClient(predicate::test)) {
throw new AssertionError("Timed out waiting for predicate");
}
+
+ return timeout;
}
}
@Override
- public void waitForScreen(@Nullable Class extends Screen> screenClass) {
+ public int waitForScreen(@Nullable Class extends Screen> screenClass) {
ThreadingImpl.checkOnGametestThread("waitForScreen");
if (screenClass == null) {
- waitFor(client -> client.currentScreen == null);
+ return waitFor(client -> client.currentScreen == null);
} else {
- waitFor(client -> screenClass.isInstance(client.currentScreen));
+ return waitFor(client -> screenClass.isInstance(client.currentScreen));
}
}
@@ -270,10 +281,15 @@ public Path takeScreenshot(String name, int delay) {
}
@Override
- public ClientGameTestInputImpl getInput() {
+ public TestInputImpl getInput() {
return input;
}
+ @Override
+ public TestWorldBuilder worldBuilder() {
+ return new TestWorldBuilderImpl(this);
+ }
+
@Override
public void restoreDefaultGameOptions() {
ThreadingImpl.checkOnGametestThread("restoreDefaultGameOptions");
diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/ClientGameTestImpl.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/ClientGameTestImpl.java
new file mode 100644
index 0000000000..2e771f6257
--- /dev/null
+++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/ClientGameTestImpl.java
@@ -0,0 +1,75 @@
+/*
+ * 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 org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.minecraft.SharedConstants;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.screen.ConfirmScreen;
+import net.minecraft.client.gui.screen.Screen;
+import net.minecraft.client.gui.screen.world.BackupPromptScreen;
+import net.minecraft.client.gui.screen.world.LevelLoadingScreen;
+import net.minecraft.text.TranslatableTextContent;
+
+import net.fabricmc.fabric.api.client.gametest.v1.ClientGameTestContext;
+
+public final class ClientGameTestImpl {
+ public static final Logger LOGGER = LoggerFactory.getLogger("fabric-client-gametest-api-v1");
+
+ private ClientGameTestImpl() {
+ }
+
+ public static void waitForWorldLoad(ClientGameTestContext context) {
+ for (int i = 0; i < SharedConstants.TICKS_PER_MINUTE; i++) {
+ if (context.computeOnClient(client -> isExperimentalWarningScreen(client.currentScreen))) {
+ context.clickScreenButton("gui.yes");
+ }
+
+ if (context.computeOnClient(client -> client.currentScreen instanceof BackupPromptScreen)) {
+ context.clickScreenButton("selectWorld.backupJoinSkipButton");
+ }
+
+ if (context.computeOnClient(ClientGameTestImpl::isWorldLoadingFinished)) {
+ return;
+ }
+
+ context.waitTick();
+ }
+
+ if (!context.computeOnClient(ClientGameTestImpl::isWorldLoadingFinished)) {
+ throw new AssertionError("Timeout loading world");
+ }
+ }
+
+ private static boolean isExperimentalWarningScreen(Screen screen) {
+ if (!(screen instanceof ConfirmScreen)) {
+ return false;
+ }
+
+ if (!(screen.getTitle().getContent() instanceof TranslatableTextContent translatableContents)) {
+ return false;
+ }
+
+ return "selectWorld.warning.experimental.title".equals(translatableContents.getKey());
+ }
+
+ private static boolean isWorldLoadingFinished(MinecraftClient client) {
+ return client.world != null && !(client.currentScreen instanceof LevelLoadingScreen);
+ }
+}
diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/DedicatedServerImplUtil.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/DedicatedServerImplUtil.java
new file mode 100644
index 0000000000..c0ab87a895
--- /dev/null
+++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/DedicatedServerImplUtil.java
@@ -0,0 +1,90 @@
+/*
+ * 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.io.BufferedWriter;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Properties;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.minecraft.server.Main;
+import net.minecraft.server.dedicated.MinecraftDedicatedServer;
+import net.minecraft.util.Util;
+
+public final class DedicatedServerImplUtil {
+ private static final Logger LOGGER = LoggerFactory.getLogger("fabric-client-gametest-api-v1");
+ private static final Properties DEFAULT_SERVER_PROPERTIES = Util.make(new Properties(), properties -> {
+ // allow non-authenticated connections from localhost
+ properties.setProperty("online-mode", "false");
+
+ // disable sync-chunk-writes on unix systems, it slows world saving down a LOT and doesn't really help anything
+ properties.setProperty("sync-chunk-writes", String.valueOf(Util.getOperatingSystem() == Util.OperatingSystem.WINDOWS));
+
+ // allow non-opped players to place blocks at spawn
+ properties.setProperty("spawn-protection", "0");
+
+ // stops other players from joining the server and interfering with the tests
+ properties.setProperty("max-players", "1");
+ });
+
+ // If this field is set, it causes the create world screen to write the level.dat file to the specified folder
+ @Nullable
+ public static Path saveLevelDataTo = null;
+ @Nullable
+ public static CompletableFuture serverFuture = null;
+
+ private DedicatedServerImplUtil() {
+ }
+
+ public static MinecraftDedicatedServer start(Properties serverProperties) {
+ setupServer(serverProperties);
+ serverFuture = new CompletableFuture<>();
+
+ new Thread(() -> Main.main(new String[0])).start();
+
+ try {
+ return serverFuture.get(10, TimeUnit.SECONDS);
+ } catch (InterruptedException | ExecutionException | TimeoutException e) {
+ throw new RuntimeException(e);
+ } finally {
+ serverFuture = null;
+ }
+ }
+
+ private static void setupServer(Properties customServerProperties) {
+ Properties serverProperties = new Properties();
+ serverProperties.putAll(DEFAULT_SERVER_PROPERTIES);
+ serverProperties.putAll(customServerProperties);
+
+ try {
+ try (BufferedWriter writer = Files.newBufferedWriter(Path.of("server.properties"))) {
+ serverProperties.store(writer, null);
+ }
+ } catch (IOException e) {
+ LOGGER.error("Failed to write server properties", e);
+ }
+ }
+}
diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/FabricClientGameTestRunner.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/FabricClientGameTestRunner.java
index 5351f24809..2f24f847e9 100644
--- a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/FabricClientGameTestRunner.java
+++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/FabricClientGameTestRunner.java
@@ -54,7 +54,7 @@ private static void checkFinalGameTestState(ClientGameTestContext context, Strin
}
context.runOnClient(client -> {
- if (client.getNetworkHandler() != null) {
+ if (client.world != null) {
throw new AssertionError("Client gametest %s finished while still connected to a server".formatted(testClassName));
}
diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/TestClientWorldContextImpl.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/TestClientWorldContextImpl.java
new file mode 100644
index 0000000000..1c5c9f2b25
--- /dev/null
+++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/TestClientWorldContextImpl.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.impl.client.gametest;
+
+import java.util.Objects;
+
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.world.ClientChunkManager;
+import net.minecraft.client.world.ClientWorld;
+import net.minecraft.world.chunk.ChunkStatus;
+
+import net.fabricmc.fabric.api.client.gametest.v1.ClientGameTestContext;
+import net.fabricmc.fabric.api.client.gametest.v1.TestClientWorldContext;
+import net.fabricmc.fabric.mixin.client.gametest.ClientChunkManagerAccessor;
+import net.fabricmc.fabric.mixin.client.gametest.ClientChunkMapAccessor;
+import net.fabricmc.fabric.mixin.client.gametest.ClientWorldAccessor;
+
+public class TestClientWorldContextImpl implements TestClientWorldContext {
+ private final ClientGameTestContext context;
+
+ public TestClientWorldContextImpl(ClientGameTestContext context) {
+ this.context = context;
+ }
+
+ @Override
+ public int waitForChunksDownload(int timeout) {
+ ThreadingImpl.checkOnGametestThread("waitForChunksDownload");
+
+ return context.waitFor(TestClientWorldContextImpl::areChunksLoaded, timeout);
+ }
+
+ @Override
+ public int waitForChunksRender(boolean waitForDownload, int timeout) {
+ ThreadingImpl.checkOnGametestThread("waitForChunksRender");
+
+ return context.waitFor(client -> (!waitForDownload || areChunksLoaded(client)) && areChunksRendered(client), timeout);
+ }
+
+ private static boolean areChunksLoaded(MinecraftClient client) {
+ int viewDistance = client.options.getClampedViewDistance();
+ ClientWorld world = Objects.requireNonNull(client.world);
+ ClientChunkManager.ClientChunkMap chunks = ((ClientChunkManagerAccessor) world.getChunkManager()).getChunks();
+ ClientChunkMapAccessor chunksAccessor = (ClientChunkMapAccessor) (Object) chunks;
+ int centerChunkX = chunksAccessor.getCenterChunkX();
+ int centerChunkZ = chunksAccessor.getCenterChunkZ();
+
+ for (int dz = -viewDistance; dz <= viewDistance; dz++) {
+ for (int dx = -viewDistance; dx <= viewDistance; dx++) {
+ if (world.getChunk(centerChunkX + dx, centerChunkZ + dz, ChunkStatus.FULL, false) == null) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ private static boolean areChunksRendered(MinecraftClient client) {
+ ClientWorld world = Objects.requireNonNull(client.world);
+ return ((ClientWorldAccessor) world).getChunkUpdaters().isEmpty() && client.worldRenderer.isTerrainRenderComplete();
+ }
+}
diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/TestDedicatedServer.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/TestDedicatedServer.java
deleted file mode 100644
index 8d6105e4b8..0000000000
--- a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/TestDedicatedServer.java
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package net.fabricmc.fabric.impl.client.gametest;
-
-import java.io.Closeable;
-import java.io.IOException;
-import java.io.UncheckedIOException;
-import java.nio.file.Files;
-import java.nio.file.Paths;
-import java.time.Duration;
-import java.util.Objects;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.atomic.AtomicReference;
-
-import net.minecraft.server.Main;
-import net.minecraft.server.dedicated.MinecraftDedicatedServer;
-
-public class TestDedicatedServer implements Closeable {
- public static final AtomicReference DEDICATED_SERVER_REF = new AtomicReference<>();
- private static final Duration START_TIMEOUT = Duration.ofMinutes(5);
-
- final ExecutorService executor = Executors.newSingleThreadExecutor();
- MinecraftDedicatedServer server;
-
- public TestDedicatedServer() {
- assert DEDICATED_SERVER_REF.get() == null : "A dedicated server is already running";
- executor.execute(this::run);
- waitUntilReady();
- Objects.requireNonNull(server);
- }
-
- public String getConnectionAddress() {
- return "localhost:" + server.getServerPort();
- }
-
- public void runCommand(String command) {
- ThreadingImpl.runOnServer(() -> server.getCommandManager().executeWithPrefix(server.getCommandSource(), command));
- }
-
- private void run() {
- setupServer();
- Main.main(new String[]{});
- }
-
- private void setupServer() {
- try {
- Files.writeString(Paths.get("eula.txt"), "eula=true");
- Files.writeString(Paths.get("server.properties"), "online-mode=false");
- } catch (IOException e) {
- throw new UncheckedIOException(e);
- }
- }
-
- private void waitUntilReady() {
- long startTime = System.currentTimeMillis();
-
- while (DEDICATED_SERVER_REF.get() == null) {
- if (System.currentTimeMillis() - startTime > START_TIMEOUT.toMillis()) {
- throw new RuntimeException("Timeout while waiting for the server to start");
- }
-
- try {
- Thread.sleep(100);
- } catch (InterruptedException e) {
- throw new RuntimeException(e);
- }
- }
-
- server = DEDICATED_SERVER_REF.get();
- DEDICATED_SERVER_REF.set(null);
- }
-
- @Override
- public void close() {
- server.stop(false);
-
- while (server.getThread().isAlive()) {
- ThreadingImpl.runTick();
- }
-
- executor.close();
- }
-}
diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/TestDedicatedServerContextImpl.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/TestDedicatedServerContextImpl.java
new file mode 100644
index 0000000000..2bb99653d5
--- /dev/null
+++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/TestDedicatedServerContextImpl.java
@@ -0,0 +1,67 @@
+/*
+ * 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 net.minecraft.client.gui.screen.multiplayer.ConnectScreen;
+import net.minecraft.client.network.ServerAddress;
+import net.minecraft.client.network.ServerInfo;
+import net.minecraft.server.dedicated.MinecraftDedicatedServer;
+
+import net.fabricmc.fabric.api.client.gametest.v1.ClientGameTestContext;
+import net.fabricmc.fabric.api.client.gametest.v1.TestClientWorldContext;
+import net.fabricmc.fabric.api.client.gametest.v1.TestDedicatedServerContext;
+import net.fabricmc.fabric.api.client.gametest.v1.TestServerConnection;
+
+public class TestDedicatedServerContextImpl extends TestServerContextImpl implements TestDedicatedServerContext {
+ private final ClientGameTestContext context;
+
+ public TestDedicatedServerContextImpl(ClientGameTestContext context, MinecraftDedicatedServer server) {
+ super(server);
+ this.context = context;
+ }
+
+ @Override
+ public TestServerConnection connect() {
+ ThreadingImpl.checkOnGametestThread("connect");
+
+ context.runOnClient(client -> {
+ final var serverInfo = new ServerInfo("localhost", getConnectionAddress(), ServerInfo.ServerType.OTHER);
+ ConnectScreen.connect(client.currentScreen, client, ServerAddress.parse(getConnectionAddress()), serverInfo, false, null);
+ });
+
+ ClientGameTestImpl.waitForWorldLoad(context);
+
+ TestClientWorldContext clientWorld = new TestClientWorldContextImpl(context);
+ return new TestServerConnectionImpl(context, clientWorld);
+ }
+
+ private String getConnectionAddress() {
+ return "localhost:" + server.getServerPort();
+ }
+
+ @Override
+ public void close() {
+ ThreadingImpl.checkOnGametestThread("close");
+
+ if (!ThreadingImpl.isServerRunning || !server.getThread().isAlive()) {
+ throw new AssertionError("Stopped the dedicated server before closing the dedicated server context");
+ }
+
+ server.stop(false);
+ context.waitFor(client -> !ThreadingImpl.isServerRunning && !server.getThread().isAlive());
+ }
+}
diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/ClientGameTestInputImpl.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/TestInputImpl.java
similarity index 97%
rename from fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/ClientGameTestInputImpl.java
rename to fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/TestInputImpl.java
index f957604411..7eaf062bd9 100644
--- a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/ClientGameTestInputImpl.java
+++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/TestInputImpl.java
@@ -30,16 +30,16 @@
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.api.client.gametest.v1.TestInput;
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 {
+public final class TestInputImpl implements TestInput {
private static final Set KEYS_DOWN = new HashSet<>();
private final ClientGameTestContext context;
- public ClientGameTestInputImpl(ClientGameTestContext context) {
+ public TestInputImpl(ClientGameTestContext context) {
this.context = context;
}
diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/TestServerConnectionImpl.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/TestServerConnectionImpl.java
new file mode 100644
index 0000000000..ef31e1816b
--- /dev/null
+++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/TestServerConnectionImpl.java
@@ -0,0 +1,54 @@
+/*
+ * 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 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.TestClientWorldContext;
+import net.fabricmc.fabric.api.client.gametest.v1.TestServerConnection;
+
+public class TestServerConnectionImpl implements TestServerConnection {
+ private final ClientGameTestContext context;
+ private final TestClientWorldContext clientWorld;
+
+ public TestServerConnectionImpl(ClientGameTestContext context, TestClientWorldContext clientWorld) {
+ this.context = context;
+ this.clientWorld = clientWorld;
+ }
+
+ @Override
+ public TestClientWorldContext getClientWorld() {
+ return clientWorld;
+ }
+
+ @Override
+ public void close() {
+ ThreadingImpl.checkOnGametestThread("close");
+
+ context.runOnClient(client -> {
+ if (client.world == null) {
+ throw new AssertionError("Disconnected from server before closing the test server connection");
+ }
+ });
+
+ context.runOnClient(MinecraftClient::disconnect);
+ context.waitFor(client -> client.world == null);
+ context.setScreen(TitleScreen::new);
+ }
+}
diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/TestServerContextImpl.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/TestServerContextImpl.java
new file mode 100644
index 0000000000..0bb892e2e9
--- /dev/null
+++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/TestServerContextImpl.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.impl.client.gametest;
+
+import com.google.common.base.Preconditions;
+import org.apache.commons.lang3.function.FailableConsumer;
+import org.apache.commons.lang3.function.FailableFunction;
+import org.apache.commons.lang3.mutable.MutableObject;
+
+import net.minecraft.server.MinecraftServer;
+
+import net.fabricmc.fabric.api.client.gametest.v1.TestServerContext;
+
+public class TestServerContextImpl implements TestServerContext {
+ protected final MinecraftServer server;
+
+ public TestServerContextImpl(MinecraftServer server) {
+ this.server = server;
+ }
+
+ @Override
+ public void runCommand(String command) {
+ ThreadingImpl.checkOnGametestThread("runCommand");
+ Preconditions.checkNotNull(command, "command");
+
+ runOnServer(server -> server.getCommandManager().executeWithPrefix(server.getCommandSource(), command));
+ }
+
+ @Override
+ public void runOnServer(FailableConsumer action) throws E {
+ ThreadingImpl.checkOnGametestThread("runOnServer");
+ Preconditions.checkNotNull(action, "action");
+
+ ThreadingImpl.runOnServer(() -> action.accept(server));
+ }
+
+ @Override
+ public T computeOnServer(FailableFunction function) throws E {
+ ThreadingImpl.checkOnGametestThread("computeOnServer");
+ Preconditions.checkNotNull(function, "function");
+
+ MutableObject result = new MutableObject<>();
+ ThreadingImpl.runOnServer(() -> result.setValue(function.apply(server)));
+ return result.getValue();
+ }
+}
diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/TestSingleplayerContextImpl.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/TestSingleplayerContextImpl.java
new file mode 100644
index 0000000000..8e79d3cfc6
--- /dev/null
+++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/TestSingleplayerContextImpl.java
@@ -0,0 +1,73 @@
+/*
+ * 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 net.minecraft.SharedConstants;
+import net.minecraft.client.gui.screen.GameMenuScreen;
+import net.minecraft.client.gui.screen.TitleScreen;
+import net.minecraft.server.MinecraftServer;
+
+import net.fabricmc.fabric.api.client.gametest.v1.ClientGameTestContext;
+import net.fabricmc.fabric.api.client.gametest.v1.TestClientWorldContext;
+import net.fabricmc.fabric.api.client.gametest.v1.TestServerContext;
+import net.fabricmc.fabric.api.client.gametest.v1.TestSingleplayerContext;
+import net.fabricmc.fabric.api.client.gametest.v1.TestWorldSave;
+
+public class TestSingleplayerContextImpl implements TestSingleplayerContext {
+ private final ClientGameTestContext context;
+ private final TestWorldSave worldSave;
+ private final TestClientWorldContext clientWorld;
+ private final TestServerContext server;
+
+ public TestSingleplayerContextImpl(ClientGameTestContext context, TestWorldSave worldSave, MinecraftServer server) {
+ this.context = context;
+ this.worldSave = worldSave;
+ this.clientWorld = new TestClientWorldContextImpl(context);
+ this.server = new TestServerContextImpl(server);
+ }
+
+ @Override
+ public TestWorldSave getWorldSave() {
+ return worldSave;
+ }
+
+ @Override
+ public TestClientWorldContext getClientWorld() {
+ return clientWorld;
+ }
+
+ @Override
+ public TestServerContext getServer() {
+ return server;
+ }
+
+ @Override
+ public void close() {
+ ThreadingImpl.checkOnGametestThread("close");
+
+ context.runOnClient(client -> {
+ if (client.world == null) {
+ throw new IllegalStateException("Exited the world before closing singleplayer context");
+ }
+ });
+
+ context.setScreen(() -> new GameMenuScreen(true));
+ context.clickScreenButton("menu.returnToMenu");
+ context.waitForScreen(TitleScreen.class);
+ context.waitFor(client -> !ThreadingImpl.isServerRunning && client.world == null, SharedConstants.TICKS_PER_MINUTE);
+ }
+}
diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/TestWorldBuilderImpl.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/TestWorldBuilderImpl.java
new file mode 100644
index 0000000000..1c21c7ff70
--- /dev/null
+++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/TestWorldBuilderImpl.java
@@ -0,0 +1,140 @@
+/*
+ * 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.io.IOException;
+import java.nio.file.Path;
+import java.util.Properties;
+import java.util.function.Consumer;
+
+import com.google.common.base.Preconditions;
+import org.apache.commons.io.FileUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.screen.world.CreateWorldScreen;
+import net.minecraft.client.gui.screen.world.WorldCreator;
+import net.minecraft.registry.RegistryKeys;
+import net.minecraft.registry.entry.RegistryEntry;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.dedicated.MinecraftDedicatedServer;
+import net.minecraft.world.GameRules;
+import net.minecraft.world.gen.WorldPreset;
+import net.minecraft.world.gen.WorldPresets;
+
+import net.fabricmc.fabric.api.client.gametest.v1.ClientGameTestContext;
+import net.fabricmc.fabric.api.client.gametest.v1.TestDedicatedServerContext;
+import net.fabricmc.fabric.api.client.gametest.v1.TestSingleplayerContext;
+import net.fabricmc.fabric.api.client.gametest.v1.TestWorldBuilder;
+import net.fabricmc.fabric.mixin.client.gametest.CreateWorldScreenAccessor;
+
+public class TestWorldBuilderImpl implements TestWorldBuilder {
+ private static final Logger LOGGER = LoggerFactory.getLogger("fabric-client-gametest-api-v1");
+ private final ClientGameTestContext context;
+ private boolean useConsistentSettings = true;
+
+ private Consumer settingsAdjustor = creator -> {
+ };
+
+ public TestWorldBuilderImpl(ClientGameTestContext context) {
+ this.context = context;
+ }
+
+ @Override
+ public TestWorldBuilder setUseConsistentSettings(boolean useConsistentSettings) {
+ this.useConsistentSettings = useConsistentSettings;
+ return this;
+ }
+
+ @Override
+ public TestWorldBuilder adjustSettings(Consumer settingsAdjuster) {
+ Preconditions.checkNotNull(settingsAdjuster, "settingsAdjuster");
+
+ this.settingsAdjustor = settingsAdjuster;
+ return this;
+ }
+
+ @Override
+ public TestSingleplayerContext create() {
+ ThreadingImpl.checkOnGametestThread("create");
+ Preconditions.checkState(!ThreadingImpl.isServerRunning, "Cannot create a world when a server is running");
+
+ Path saveDirectory = navigateCreateWorldScreen();
+ ClientGameTestImpl.waitForWorldLoad(context);
+
+ MinecraftServer server = context.computeOnClient(MinecraftClient::getServer);
+ return new TestSingleplayerContextImpl(context, new TestWorldSaveImpl(context, saveDirectory), server);
+ }
+
+ @Override
+ public TestDedicatedServerContext createServer(Properties serverProperties) {
+ ThreadingImpl.checkOnGametestThread("createServer");
+ Preconditions.checkState(!ThreadingImpl.isServerRunning, "Cannot create a server when a server is running");
+
+ DedicatedServerImplUtil.saveLevelDataTo = Path.of(serverProperties.getProperty("level-name", "world"));
+
+ try {
+ FileUtils.deleteDirectory(DedicatedServerImplUtil.saveLevelDataTo.toFile());
+ } catch (IOException e) {
+ LOGGER.error("Failed to clean up old dedicated server world", e);
+ }
+
+ try {
+ navigateCreateWorldScreen();
+ } finally {
+ DedicatedServerImplUtil.saveLevelDataTo = null;
+ }
+
+ MinecraftDedicatedServer server = DedicatedServerImplUtil.start(serverProperties);
+ return new TestDedicatedServerContextImpl(context, server);
+ }
+
+ private Path navigateCreateWorldScreen() {
+ Path saveDirectory = context.computeOnClient(client -> {
+ CreateWorldScreen.show(client, client.currentScreen);
+
+ if (!(client.currentScreen instanceof CreateWorldScreen createWorldScreen)) {
+ throw new AssertionError("CreateWorldScreen.show did not set the current screen");
+ }
+
+ WorldCreator creator = ((CreateWorldScreenAccessor) createWorldScreen).getWorldCreator();
+
+ if (useConsistentSettings) {
+ setConsistentSettings(creator);
+ }
+
+ settingsAdjustor.accept(creator);
+
+ return client.getLevelStorage().getSavesDirectory().resolve(creator.getWorldDirectoryName());
+ });
+
+ context.clickScreenButton("selectWorld.create");
+
+ return saveDirectory;
+ }
+
+ private static void setConsistentSettings(WorldCreator creator) {
+ RegistryEntry flatPreset = creator.getGeneratorOptionsHolder().getCombinedRegistryManager().getOrThrow(RegistryKeys.WORLD_PRESET).getOrThrow(WorldPresets.FLAT);
+ creator.setWorldType(new WorldCreator.WorldType(flatPreset));
+ creator.setSeed("1");
+ creator.setGenerateStructures(false);
+ creator.getGameRules().get(GameRules.DO_DAYLIGHT_CYCLE).set(false, null);
+ creator.getGameRules().get(GameRules.DO_WEATHER_CYCLE).set(false, null);
+ creator.getGameRules().get(GameRules.DO_MOB_SPAWNING).set(false, null);
+ }
+}
diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/TestWorldSaveImpl.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/TestWorldSaveImpl.java
new file mode 100644
index 0000000000..0c60f8f26e
--- /dev/null
+++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/TestWorldSaveImpl.java
@@ -0,0 +1,60 @@
+/*
+ * 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 com.google.common.base.Preconditions;
+
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.server.MinecraftServer;
+
+import net.fabricmc.fabric.api.client.gametest.v1.ClientGameTestContext;
+import net.fabricmc.fabric.api.client.gametest.v1.TestSingleplayerContext;
+import net.fabricmc.fabric.api.client.gametest.v1.TestWorldSave;
+
+public final class TestWorldSaveImpl implements TestWorldSave {
+ private final ClientGameTestContext context;
+ private final Path saveDirectory;
+
+ public TestWorldSaveImpl(ClientGameTestContext context, Path saveDirectory) {
+ this.context = context;
+ this.saveDirectory = saveDirectory;
+ }
+
+ @Override
+ public Path getSaveDirectory() {
+ return saveDirectory;
+ }
+
+ @Override
+ public TestSingleplayerContext open() {
+ ThreadingImpl.checkOnGametestThread("open");
+ Preconditions.checkState(!ThreadingImpl.isServerRunning, "Cannot open a world when a server is running");
+
+ context.runOnClient(client -> {
+ client.createIntegratedServerLoader().start(saveDirectory.getFileName().toString(), () -> {
+ throw new AssertionError("Level loading should not be canceled");
+ });
+ });
+
+ ClientGameTestImpl.waitForWorldLoad(context);
+
+ MinecraftServer server = context.computeOnClient(MinecraftClient::getServer);
+ return new TestSingleplayerContextImpl(context, this, server);
+ }
+}
diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/ClientChunkManagerAccessor.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/ClientChunkManagerAccessor.java
new file mode 100644
index 0000000000..3b5f5148bc
--- /dev/null
+++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/ClientChunkManagerAccessor.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.mixin.client.gametest;
+
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.gen.Accessor;
+
+import net.minecraft.client.world.ClientChunkManager;
+
+@Mixin(ClientChunkManager.class)
+public interface ClientChunkManagerAccessor {
+ @Accessor
+ ClientChunkManager.ClientChunkMap getChunks();
+}
diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/ClientChunkMapAccessor.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/ClientChunkMapAccessor.java
new file mode 100644
index 0000000000..9680970a1a
--- /dev/null
+++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/ClientChunkMapAccessor.java
@@ -0,0 +1,31 @@
+/*
+ * 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.world.ClientChunkManager;
+
+@Mixin(ClientChunkManager.ClientChunkMap.class)
+public interface ClientChunkMapAccessor {
+ @Accessor
+ int getCenterChunkX();
+
+ @Accessor
+ int getCenterChunkZ();
+}
diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/ClientWorldAccessor.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/ClientWorldAccessor.java
new file mode 100644
index 0000000000..f18ee1b847
--- /dev/null
+++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/ClientWorldAccessor.java
@@ -0,0 +1,30 @@
+/*
+ * 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 java.util.Deque;
+
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.gen.Accessor;
+
+import net.minecraft.client.world.ClientWorld;
+
+@Mixin(ClientWorld.class)
+public interface ClientWorldAccessor {
+ @Accessor
+ Deque getChunkUpdaters();
+}
diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/CreateWorldScreenAccessor.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/CreateWorldScreenAccessor.java
new file mode 100644
index 0000000000..b779fda96e
--- /dev/null
+++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/CreateWorldScreenAccessor.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.mixin.client.gametest;
+
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.gen.Accessor;
+
+import net.minecraft.client.gui.screen.world.CreateWorldScreen;
+import net.minecraft.client.gui.screen.world.WorldCreator;
+
+@Mixin(CreateWorldScreen.class)
+public interface CreateWorldScreenAccessor {
+ @Accessor
+ WorldCreator getWorldCreator();
+}
diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/CreateWorldScreenMixin.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/CreateWorldScreenMixin.java
new file mode 100644
index 0000000000..ced1114b52
--- /dev/null
+++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/CreateWorldScreenMixin.java
@@ -0,0 +1,57 @@
+/*
+ * 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 java.io.IOException;
+import java.nio.file.Files;
+
+import com.llamalad7.mixinextras.sugar.Local;
+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.gui.screen.world.CreateWorldScreen;
+import net.minecraft.nbt.NbtCompound;
+import net.minecraft.nbt.NbtIo;
+import net.minecraft.registry.CombinedDynamicRegistries;
+import net.minecraft.registry.ServerDynamicRegistryType;
+import net.minecraft.world.level.LevelProperties;
+
+import net.fabricmc.fabric.impl.client.gametest.ClientGameTestImpl;
+import net.fabricmc.fabric.impl.client.gametest.DedicatedServerImplUtil;
+
+@Mixin(CreateWorldScreen.class)
+public class CreateWorldScreenMixin {
+ @Inject(method = "createLevel", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/integrated/IntegratedServerLoader;tryLoad(Lnet/minecraft/client/MinecraftClient;Lnet/minecraft/client/gui/screen/world/CreateWorldScreen;Lcom/mojang/serialization/Lifecycle;Ljava/lang/Runnable;Z)V"), cancellable = true)
+ private void createLevelDataForServers(CallbackInfo ci, @Local CombinedDynamicRegistries dynamicRegistries, @Local LevelProperties levelProperties) {
+ if (DedicatedServerImplUtil.saveLevelDataTo != null) {
+ NbtCompound levelDatInner = levelProperties.cloneWorldNbt(dynamicRegistries.getCombinedRegistryManager(), null);
+ NbtCompound levelDat = new NbtCompound();
+ levelDat.put("Data", levelDatInner);
+
+ try {
+ Files.createDirectories(DedicatedServerImplUtil.saveLevelDataTo);
+ NbtIo.writeCompressed(levelDat, DedicatedServerImplUtil.saveLevelDataTo.resolve("level.dat"));
+ } catch (IOException e) {
+ ClientGameTestImpl.LOGGER.error("Failed to save dedicated server level data", e);
+ }
+
+ ci.cancel();
+ }
+ }
+}
diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/InputUtilMixin.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/InputUtilMixin.java
index 0351fa1b75..3b031f19c6 100644
--- a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/InputUtilMixin.java
+++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/InputUtilMixin.java
@@ -24,13 +24,13 @@
import net.minecraft.client.util.InputUtil;
-import net.fabricmc.fabric.impl.client.gametest.ClientGameTestInputImpl;
+import net.fabricmc.fabric.impl.client.gametest.TestInputImpl;
@Mixin(InputUtil.class)
public class InputUtilMixin {
@Inject(method = "isKeyPressed", at = @At("HEAD"), cancellable = true)
private static void useGameTestInputForKeyPressed(long window, int keyCode, CallbackInfoReturnable cir) {
- cir.setReturnValue(ClientGameTestInputImpl.isKeyDown(keyCode));
+ cir.setReturnValue(TestInputImpl.isKeyDown(keyCode));
}
@Inject(method = {"setKeyboardCallbacks", "setMouseCallbacks"}, at = @At("HEAD"), cancellable = true)
diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/MinecraftDedicatedServerMixin.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/MinecraftDedicatedServerMixin.java
index 19575f6069..8bf1065de7 100644
--- a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/MinecraftDedicatedServerMixin.java
+++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/MinecraftDedicatedServerMixin.java
@@ -16,6 +16,8 @@
package net.fabricmc.fabric.mixin.client.gametest;
+import java.util.concurrent.CompletableFuture;
+
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import org.spongepowered.asm.mixin.Mixin;
@@ -25,14 +27,18 @@
import net.minecraft.server.dedicated.MinecraftDedicatedServer;
-import net.fabricmc.fabric.impl.client.gametest.TestDedicatedServer;
+import net.fabricmc.fabric.impl.client.gametest.DedicatedServerImplUtil;
@Mixin(MinecraftDedicatedServer.class)
public abstract class MinecraftDedicatedServerMixin {
@Inject(method = "setupServer", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/dedicated/MinecraftDedicatedServer;loadWorld()V"))
private void captureServerInstance(CallbackInfoReturnable cir) {
// Capture the server instance once the server is ready to be connected to
- TestDedicatedServer.DEDICATED_SERVER_REF.set((MinecraftDedicatedServer) (Object) this);
+ CompletableFuture serverFuture = DedicatedServerImplUtil.serverFuture;
+
+ if (serverFuture != null) {
+ serverFuture.complete((MinecraftDedicatedServer) (Object) this);
+ }
}
// Don't call shutdownExecutors as we are running the dedi server within the client process.
diff --git a/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/ServerMainMixin.java b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/ServerMainMixin.java
new file mode 100644
index 0000000000..0715e4f3d2
--- /dev/null
+++ b/fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/ServerMainMixin.java
@@ -0,0 +1,31 @@
+/*
+ * 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 com.llamalad7.mixinextras.injector.v2.WrapWithCondition;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+
+import net.minecraft.server.Main;
+
+@Mixin(Main.class)
+public class ServerMainMixin {
+ @WrapWithCondition(method = "main", remap = false, at = @At(value = "INVOKE", target = "Lnet/minecraft/util/Util;startTimerHack()V", remap = true))
+ private static boolean dontStartAnotherTimerHack() {
+ return false;
+ }
+}
diff --git a/fabric-client-gametest-api-v1/src/client/resources/fabric-client-gametest-api-v1.accesswidener b/fabric-client-gametest-api-v1/src/client/resources/fabric-client-gametest-api-v1.accesswidener
index 622ee90f61..f4add24c31 100644
--- a/fabric-client-gametest-api-v1/src/client/resources/fabric-client-gametest-api-v1.accesswidener
+++ b/fabric-client-gametest-api-v1/src/client/resources/fabric-client-gametest-api-v1.accesswidener
@@ -1,2 +1,3 @@
accessWidener v2 named
accessible class net/minecraft/client/option/GameOptions$Visitor
+accessible class net/minecraft/client/world/ClientChunkManager$ClientChunkMap
diff --git a/fabric-client-gametest-api-v1/src/client/resources/fabric-client-gametest-api-v1.mixins.json b/fabric-client-gametest-api-v1/src/client/resources/fabric-client-gametest-api-v1.mixins.json
index 5b0dbb8113..93213cb446 100644
--- a/fabric-client-gametest-api-v1/src/client/resources/fabric-client-gametest-api-v1.mixins.json
+++ b/fabric-client-gametest-api-v1/src/client/resources/fabric-client-gametest-api-v1.mixins.json
@@ -14,10 +14,18 @@
"MinecraftServerMixin",
"MouseAccessor",
"ScreenAccessor",
+ "ServerMainMixin",
"WindowMixin"
],
"plugin": "net.fabricmc.fabric.impl.client.gametest.ClientGameTestMixinConfigPlugin",
"injectors": {
"defaultRequire": 1
- }
+ },
+ "client": [
+ "ClientChunkManagerAccessor",
+ "ClientChunkMapAccessor",
+ "ClientWorldAccessor",
+ "CreateWorldScreenAccessor",
+ "CreateWorldScreenMixin"
+ ]
}
diff --git a/fabric-client-gametest-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/client/gametest/ClientGameTestTest.java b/fabric-client-gametest-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/client/gametest/ClientGameTestTest.java
index 61956ad959..eb941111ec 100644
--- a/fabric-client-gametest-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/client/gametest/ClientGameTestTest.java
+++ b/fabric-client-gametest-api-v1/src/testmodClient/java/net/fabricmc/fabric/test/client/gametest/ClientGameTestTest.java
@@ -16,141 +16,85 @@
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.gui.screen.world.WorldCreator;
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.api.client.gametest.v1.TestDedicatedServerContext;
+import net.fabricmc.fabric.api.client.gametest.v1.TestServerConnection;
+import net.fabricmc.fabric.api.client.gametest.v1.TestSingleplayerContext;
+import net.fabricmc.fabric.api.client.gametest.v1.TestWorldSave;
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);
- }
+ TestWorldSave spWorldSave;
+ try (TestSingleplayerContext singleplayer = context.worldBuilder()
+ .adjustSettings(creator -> creator.setGameMode(WorldCreator.Mode.CREATIVE)).create()) {
+ spWorldSave = singleplayer.getWorldSave();
- try (var server = new TestDedicatedServer()) {
- connectToServer(context, server);
- waitForWorldTicks(context, 5);
+ {
+ enableDebugHud(context);
+ singleplayer.getClientWorld().waitForChunksRender();
+ context.takeScreenshot("in_game_overworld", 0);
+ }
- final GameProfile profile = context.computeOnClient(MinecraftClient::getGameProfile);
- server.runCommand("op " + profile.getName());
- server.runCommand("gamemode creative " + profile.getName());
+ {
+ 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);
+ }
- waitForWorldTicks(context, 20);
- context.takeScreenshot("server_in_game", 0);
+ MixinEnvironment.getCurrentEnvironment().audit();
- { // 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);
+ {
+ // 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.setScreen(() -> new GameMenuScreen(true));
- context.takeScreenshot("server_game_menu");
- context.clickScreenButton("menu.disconnect");
-
- context.waitForScreen(MultiplayerScreen.class);
- context.clickScreenButton("gui.back");
+ {
+ context.getInput().pressKey(options -> options.inventoryKey);
+ context.takeScreenshot("in_game_inventory");
+ context.setScreen(() -> null);
+ }
}
- {
- context.waitForScreen(TitleScreen.class);
+ try (TestSingleplayerContext singleplayer = spWorldSave.open()) {
+ singleplayer.getClientWorld().waitForChunksRender();
+ context.takeScreenshot("in_game_overworld_2");
}
- }
- private static boolean isDirEmpty(Path path) {
- try (DirectoryStream directory = Files.newDirectoryStream(path)) {
- return !directory.iterator().hasNext();
- } catch (IOException e) {
- throw new UncheckedIOException(e);
+ try (TestDedicatedServerContext server = context.worldBuilder().createServer()) {
+ try (TestServerConnection connection = server.connect()) {
+ connection.getClientWorld().waitForChunksRender();
+ context.takeScreenshot("server_in_game", 0);
+
+ { // Test that we can enter and exit configuration
+ final GameProfile profile = context.computeOnClient(MinecraftClient::getGameProfile);
+ server.runCommand("debugconfig config " + profile.getName());
+ context.waitForScreen(ReconfiguringScreen.class);
+ context.takeScreenshot("server_config");
+ server.runCommand("debugconfig unconfig " + profile.getId());
+ // TODO: better way to wait for reconfiguration to end
+ context.waitTicks(100);
+ }
+ }
}
}
@@ -167,25 +111,4 @@ private static void enableDebugHud(ClientGameTestContext context) {
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);
- }
}