From 99ff640a5d543bab0316924fb669304c25d8f293 Mon Sep 17 00:00:00 2001 From: Joseph Burton Date: Tue, 24 Dec 2024 13:23:52 +0000 Subject: [PATCH] Add various lifetime-bound try-with-resources APIs to client gametests (#4318) * Add various lifetime-bound try-with-resources APIs to client gametests --- .github/workflows/build.yml | 1 + .../gametest/v1/ClientGameTestContext.java | 23 ++- .../gametest/v1/TestClientWorldContext.java | 103 ++++++++++ .../v1/TestDedicatedServerContext.java | 43 +++++ ...lientGameTestInput.java => TestInput.java} | 2 +- .../gametest/v1/TestServerConnection.java | 42 +++++ .../client/gametest/v1/TestServerContext.java | 59 ++++++ .../gametest/v1/TestSingleplayerContext.java | 54 ++++++ .../client/gametest/v1/TestWorldBuilder.java | 79 ++++++++ .../api/client/gametest/v1/TestWorldSave.java | 42 +++++ .../api/client/gametest/v1/package-info.java | 132 +++++++++++++ .../gametest/ClientGameTestContextImpl.java | 34 +++- .../client/gametest/ClientGameTestImpl.java | 75 ++++++++ .../gametest/DedicatedServerImplUtil.java | 90 +++++++++ .../gametest/FabricClientGameTestRunner.java | 2 +- .../gametest/TestClientWorldContextImpl.java | 76 ++++++++ .../client/gametest/TestDedicatedServer.java | 98 ---------- .../TestDedicatedServerContextImpl.java | 67 +++++++ ...eTestInputImpl.java => TestInputImpl.java} | 6 +- .../gametest/TestServerConnectionImpl.java | 54 ++++++ .../gametest/TestServerContextImpl.java | 60 ++++++ .../gametest/TestSingleplayerContextImpl.java | 73 ++++++++ .../client/gametest/TestWorldBuilderImpl.java | 140 ++++++++++++++ .../client/gametest/TestWorldSaveImpl.java | 60 ++++++ .../gametest/ClientChunkManagerAccessor.java | 28 +++ .../gametest/ClientChunkMapAccessor.java | 31 +++ .../client/gametest/ClientWorldAccessor.java | 30 +++ .../gametest/CreateWorldScreenAccessor.java | 29 +++ .../gametest/CreateWorldScreenMixin.java | 57 ++++++ .../mixin/client/gametest/InputUtilMixin.java | 4 +- .../MinecraftDedicatedServerMixin.java | 10 +- .../client/gametest/ServerMainMixin.java | 31 +++ ...abric-client-gametest-api-v1.accesswidener | 1 + .../fabric-client-gametest-api-v1.mixins.json | 10 +- .../client/gametest/ClientGameTestTest.java | 177 +++++------------- 35 files changed, 1573 insertions(+), 250 deletions(-) create mode 100644 fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/TestClientWorldContext.java create mode 100644 fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/TestDedicatedServerContext.java rename fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/{ClientGameTestInput.java => TestInput.java} (99%) create mode 100644 fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/TestServerConnection.java create mode 100644 fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/TestServerContext.java create mode 100644 fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/TestSingleplayerContext.java create mode 100644 fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/TestWorldBuilder.java create mode 100644 fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/api/client/gametest/v1/TestWorldSave.java create mode 100644 fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/ClientGameTestImpl.java create mode 100644 fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/DedicatedServerImplUtil.java create mode 100644 fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/TestClientWorldContextImpl.java delete mode 100644 fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/TestDedicatedServer.java create mode 100644 fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/TestDedicatedServerContextImpl.java rename fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/{ClientGameTestInputImpl.java => TestInputImpl.java} (97%) create mode 100644 fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/TestServerConnectionImpl.java create mode 100644 fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/TestServerContextImpl.java create mode 100644 fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/TestSingleplayerContextImpl.java create mode 100644 fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/TestWorldBuilderImpl.java create mode 100644 fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/impl/client/gametest/TestWorldSaveImpl.java create mode 100644 fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/ClientChunkManagerAccessor.java create mode 100644 fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/ClientChunkMapAccessor.java create mode 100644 fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/ClientWorldAccessor.java create mode 100644 fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/CreateWorldScreenAccessor.java create mode 100644 fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/CreateWorldScreenMixin.java create mode 100644 fabric-client-gametest-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/gametest/ServerMainMixin.java 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 screenClass); + int waitForScreen(@Nullable Class 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 nameGametest defaultVanilla defaultReason
{@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 nameGametest defaultVanilla defaultReason
{@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 valueConsistency 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 nameGametest defaultVanilla defaultReason
{@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 screenClass) { + public int waitForScreen(@Nullable Class 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); - } }