Add various lifetime-bound try-with-resources APIs to client gametests ()

* Add various lifetime-bound try-with-resources APIs to client gametests
This commit is contained in:
Joseph Burton 2024-12-24 13:23:52 +00:00 committed by GitHub
parent efa825c9d7
commit 99ff640a5d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 1575 additions and 252 deletions

View file

@ -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:

View file

@ -30,8 +30,9 @@ import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.screen.Screen;
/**
* Context for a client gametest containing various helpful functions and functions to access the game. Functions in
* this class can only be called on the client gametest thread.
* Context for a client gametest containing various helpful functions and functions to access the game.
*
* <p>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<MinecraftClient> predicate);
int waitFor(Predicate<MinecraftClient> 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<MinecraftClient> predicate, int timeout);
int waitFor(Predicate<MinecraftClient> predicate, int timeout);
/**
* Waits for the given screen class to be shown. If {@code screenClass} is {@code null}, waits for the current
* screen to be {@code null}. Fails if the screen does not open after {@link #DEFAULT_TIMEOUT} ticks.
*
* @param screenClass The screen class to wait to open
* @return The number of ticks waited
*/
void waitForScreen(@Nullable Class<? extends Screen> screenClass);
int waitForScreen(@Nullable Class<? extends Screen> screenClass);
/**
* Opens a {@link Screen} on the client.
@ -126,7 +130,14 @@ public interface ClientGameTestContext {
*
* @return The client gametest input handler
*/
ClientGameTestInput getInput();
TestInput getInput();
/**
* Creates a world builder for creating singleplayer worlds and dedicated servers.
*
* @return A new world builder
*/
TestWorldBuilder worldBuilder();
/**
* Restores all game options in {@link MinecraftClient#options} to their default values for client gametests. This

View file

@ -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.
*
* <p>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);
}

View file

@ -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.
*
* <p>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();
}

View file

@ -29,7 +29,7 @@ import net.minecraft.client.util.InputUtil;
* 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.

View file

@ -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.
*
* <p>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();
}

View file

@ -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.
*
* <p>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 <E> The type of the checked exception that the action throws
* @throws E When the action throws an exception
*/
<E extends Throwable> void runOnServer(FailableConsumer<MinecraftServer, E> 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 <T> The type of the value to return
* @param <E> The type of the checked exception that the function throws
* @throws E When the function throws an exception
*/
<T, E extends Throwable> T computeOnServer(FailableFunction<MinecraftServer, T, E> function) throws E;
}

View file

@ -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.
*
* <p>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();
}

View file

@ -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.
*
* <p>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.
*
* <p>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<WorldCreator> 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);
}

View file

@ -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();
}

View file

@ -21,6 +21,138 @@
* <p>A few changes have been made to how the vanilla game threads run, to make tests more reproducible. Notably, there
* is exactly one server tick per client tick while a server is running (singleplayer or multiplayer). On singleplayer,
* packets will always arrive on a consistent tick.
*
* <h1>Default settings</h1>
* 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.
*
* <h2>Game options</h2>
* <table>
* <tr>
* <th>Setting name</th>
* <th>Gametest default</th>
* <th>Vanilla default</th>
* <th>Reason</th>
* </tr>
* <tr>
* <td>{@linkplain net.minecraft.client.option.GameOptions#tutorialStep Tutorial step}</td>
* <td>{@link net.minecraft.client.tutorial.TutorialStep#NONE NONE}</td>
* <td>{@link net.minecraft.client.tutorial.TutorialStep#MOVEMENT MOVEMENT}</td>
* <td>Consistency of tests</td>
* </tr>
* <tr>
* <td>{@linkplain net.minecraft.client.option.GameOptions#getCloudRenderMode() Cloud render mode}</td>
* <td>{@link net.minecraft.client.option.CloudRenderMode#OFF OFF}</td>
* <td>{@link net.minecraft.client.option.CloudRenderMode#FANCY FANCY}</td>
* <td>Consistency of tests</td>
* </tr>
* <tr>
* <td>{@linkplain net.minecraft.client.option.GameOptions#onboardAccessibility Onboard accessibility}</td>
* <td>{@code false}</td>
* <td>{@code true}</td>
* <td>Would cause the game test runner to have to click through the onboard accessibility prompt</td>
* </tr>
* <tr>
* <td>{@linkplain net.minecraft.client.option.GameOptions#getViewDistance() View distance}</td>
* <td>{@code 5}</td>
* <td>{@code 10}</td>
* <td>Speeds up loading of chunks, especially for functions such as
* {@link net.fabricmc.fabric.api.client.gametest.v1.TestClientWorldContext#waitForChunksRender() TestClientWorldContext.waitForChunksRender()}</td>
* </tr>
* <tr>
* <td>{@linkplain net.minecraft.client.option.GameOptions#getSoundVolumeOption(net.minecraft.sound.SoundCategory) Music volume}</td>
* <td>{@code 0.0}</td>
* <td>{@code 1.0}</td>
* <td>The game music is annoying while running gametests</td>
* </tr>
* </table>
*
* <h2>World creation options</h2>
* 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}.
*
* <table>
* <tr>
* <th>Setting name</th>
* <th>Gametest default</th>
* <th>Vanilla default</th>
* <th>Reason</th>
* </tr>
* <tr>
* <td>{@linkplain net.minecraft.client.gui.screen.world.WorldCreator#setWorldType(net.minecraft.client.gui.screen.world.WorldCreator.WorldType) World type}</td>
* <td>{@link net.minecraft.world.gen.WorldPresets#FLAT FLAT}</td>
* <td>{@link net.minecraft.world.gen.WorldPresets#DEFAULT DEFAULT}</td>
* <td>Creates cleaner test cases</td>
* </tr>
* <tr>
* <td>{@linkplain net.minecraft.client.gui.screen.world.WorldCreator#setSeed(String) Seed}</td>
* <td>{@code 1}</td>
* <td>Random value</td>
* <td>Consistency of tests</td>
* </tr>
* <tr>
* <td>{@linkplain net.minecraft.client.gui.screen.world.WorldCreator#setGenerateStructures(boolean) Generate structures}</td>
* <td>{@code false}</td>
* <td>{@code true}</td>
* <td>Consistency of tests and creates cleaner tests</td>
* </tr>
* <tr>
* <td>{@linkplain net.minecraft.world.GameRules#DO_DAYLIGHT_CYCLE Do daylight cycle}</td>
* <td>{@code false}</td>
* <td>{@code true}</td>
* <td>Consistency of tests</td>
* </tr>
* <tr>
* <td>{@linkplain net.minecraft.world.GameRules#DO_WEATHER_CYCLE Do weather cycle}</td>
* <td>{@code false}</td>
* <td>{@code true}</td>
* <td>Consistency of tests</td>
* </tr>
* <tr>
* <td>{@linkplain net.minecraft.world.GameRules#DO_MOB_SPAWNING Do mob spawning}</td>
* <td>{@code false}</td>
* <td>{@code true}</td>
* <td>Consistency of tests</td>
* </tr>
* </table>
*
* <h2>Dedicated server properties</h2>
* <table>
* <tr>
* <th>Setting name</th>
* <th>Gametest default</th>
* <th>Vanilla default</th>
* <th>Reason</th>
* </tr>
* <tr>
* <td>{@code online-mode}</td>
* <td>{@code false}</td>
* <td>{@code true}</td>
* <td>Allows the gametest client to connect to the dedicated server without being logged in to a Minecraft
* account</td>
* </tr>
* <tr>
* <td>{@code sync-chunk-writes}</td>
* <td>{@code true} on Windows, {@code false} on other operating systems</td>
* <td>{@code true}</td>
* <td>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.</td>
* </tr>
* <tr>
* <td>{@code spawn-protection}</td>
* <td>{@code 0}</td>
* <td>{@code 16}</td>
* <td>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</td>
* </tr>
* <tr>
* <td>{@code max-players}</td>
* <td>{@code 1}</td>
* <td>{@code 20}</td>
* <td>Stops other players from joining the server and interfering with the test</td>
* </tr>
* </table>
*/
@ApiStatus.Experimental
package net.fabricmc.fabric.api.client.gametest.v1;

View file

@ -48,13 +48,14 @@ import net.minecraft.text.Text;
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<String, Object> DEFAULT_GAME_OPTIONS = new HashMap<>();
@ -66,6 +67,9 @@ public final class ClientGameTestContextImpl implements ClientGameTestContext {
// 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 final class ClientGameTestContextImpl implements ClientGameTestContext {
}
@Override
public void waitFor(Predicate<MinecraftClient> predicate) {
public int waitFor(Predicate<MinecraftClient> predicate) {
ThreadingImpl.checkOnGametestThread("waitFor");
Preconditions.checkNotNull(predicate, "predicate");
waitFor(predicate, DEFAULT_TIMEOUT);
return waitFor(predicate, DEFAULT_TIMEOUT);
}
@Override
public void waitFor(Predicate<MinecraftClient> predicate, int timeout) {
public int waitFor(Predicate<MinecraftClient> 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 final class ClientGameTestContextImpl implements ClientGameTestContext {
if (!computeOnClient(predicate::test)) {
throw new AssertionError("Timed out waiting for predicate");
}
return timeout;
}
}
@Override
public void waitForScreen(@Nullable Class<? extends Screen> screenClass) {
public int waitForScreen(@Nullable Class<? extends Screen> screenClass) {
ThreadingImpl.checkOnGametestThread("waitForScreen");
if (screenClass == null) {
waitFor(client -> client.currentScreen == null);
return waitFor(client -> client.currentScreen == null);
} else {
waitFor(client -> screenClass.isInstance(client.currentScreen));
return waitFor(client -> screenClass.isInstance(client.currentScreen));
}
}
@ -270,10 +281,15 @@ public final class ClientGameTestContextImpl implements ClientGameTestContext {
}
@Override
public ClientGameTestInputImpl getInput() {
public TestInputImpl getInput() {
return input;
}
@Override
public TestWorldBuilder worldBuilder() {
return new TestWorldBuilderImpl(this);
}
@Override
public void restoreDefaultGameOptions() {
ThreadingImpl.checkOnGametestThread("restoreDefaultGameOptions");

View file

@ -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);
}
}

View file

@ -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<MinecraftDedicatedServer> 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);
}
}
}

View file

@ -54,7 +54,7 @@ public class FabricClientGameTestRunner {
}
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));
}

View file

@ -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();
}
}

View file

@ -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<MinecraftDedicatedServer> 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();
}
}

View file

@ -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());
}
}

View file

@ -30,16 +30,16 @@ import net.minecraft.client.option.KeyBinding;
import net.minecraft.client.util.InputUtil;
import net.fabricmc.fabric.api.client.gametest.v1.ClientGameTestContext;
import net.fabricmc.fabric.api.client.gametest.v1.ClientGameTestInput;
import net.fabricmc.fabric.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<InputUtil.Key> KEYS_DOWN = new HashSet<>();
private final ClientGameTestContext context;
public ClientGameTestInputImpl(ClientGameTestContext context) {
public TestInputImpl(ClientGameTestContext context) {
this.context = context;
}

View file

@ -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);
}
}

View file

@ -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 <E extends Throwable> void runOnServer(FailableConsumer<MinecraftServer, E> action) throws E {
ThreadingImpl.checkOnGametestThread("runOnServer");
Preconditions.checkNotNull(action, "action");
ThreadingImpl.runOnServer(() -> action.accept(server));
}
@Override
public <T, E extends Throwable> T computeOnServer(FailableFunction<MinecraftServer, T, E> function) throws E {
ThreadingImpl.checkOnGametestThread("computeOnServer");
Preconditions.checkNotNull(function, "function");
MutableObject<T> result = new MutableObject<>();
ThreadingImpl.runOnServer(() -> result.setValue(function.apply(server)));
return result.getValue();
}
}

View file

@ -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);
}
}

View file

@ -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<WorldCreator> 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<WorldCreator> 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<WorldPreset> 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);
}
}

View file

@ -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);
}
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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<Runnable> getChunkUpdaters();
}

View file

@ -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();
}

View file

@ -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<ServerDynamicRegistryType> 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();
}
}
}

View file

@ -24,13 +24,13 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
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<Boolean> cir) {
cir.setReturnValue(ClientGameTestInputImpl.isKeyDown(keyCode));
cir.setReturnValue(TestInputImpl.isKeyDown(keyCode));
}
@Inject(method = {"setKeyboardCallbacks", "setMouseCallbacks"}, at = @At("HEAD"), cancellable = true)

View file

@ -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 org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
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<Boolean> cir) {
// Capture the server instance once the server is ready to be connected to
TestDedicatedServer.DEDICATED_SERVER_REF.set((MinecraftDedicatedServer) (Object) this);
CompletableFuture<MinecraftDedicatedServer> 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.

View file

@ -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;
}
}

View file

@ -1,2 +1,3 @@
accessWidener v2 named
accessible class net/minecraft/client/option/GameOptions$Visitor
accessible class net/minecraft/client/world/ClientChunkManager$ClientChunkMap

View file

@ -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"
]
}

View file

@ -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");
}
TestWorldSave spWorldSave;
try (TestSingleplayerContext singleplayer = context.worldBuilder()
.adjustSettings(creator -> creator.setGameMode(WorldCreator.Mode.CREATIVE)).create()) {
spWorldSave = singleplayer.getWorldSave();
{
context.waitForScreen(CreateWorldScreen.class);
context.clickScreenButton("selectWorld.gameMode");
context.clickScreenButton("selectWorld.gameMode");
context.takeScreenshot("create_world_screen");
context.clickScreenButton("selectWorld.create");
}
{
// API test mods use experimental features
context.waitForScreen(ConfirmScreen.class);
context.clickScreenButton("gui.yes");
}
{
enableDebugHud(context);
waitForWorldTicks(context, 200);
context.takeScreenshot("in_game_overworld", 0);
}
{
context.getInput().pressKey(options -> options.chatKey);
context.waitTick();
context.getInput().typeChars("Hello, World!");
context.getInput().pressKey(InputUtil.GLFW_KEY_ENTER);
context.takeScreenshot("chat_message_sent", 5);
}
MixinEnvironment.getCurrentEnvironment().audit();
{
// See if the player render events are working.
setPerspective(context, Perspective.THIRD_PERSON_BACK);
context.takeScreenshot("in_game_overworld_third_person");
setPerspective(context, Perspective.FIRST_PERSON);
}
{
context.getInput().pressKey(options -> options.inventoryKey);
context.takeScreenshot("in_game_inventory");
context.setScreen(() -> null);
}
{
context.setScreen(() -> new GameMenuScreen(true));
context.takeScreenshot("game_menu");
context.clickScreenButton("menu.returnToMenu");
context.waitForScreen(TitleScreen.class);
waitForServerStop(context);
}
try (var server = new TestDedicatedServer()) {
connectToServer(context, server);
waitForWorldTicks(context, 5);
final GameProfile profile = context.computeOnClient(MinecraftClient::getGameProfile);
server.runCommand("op " + profile.getName());
server.runCommand("gamemode creative " + profile.getName());
waitForWorldTicks(context, 20);
context.takeScreenshot("server_in_game", 0);
{ // Test that we can enter and exit configuration
server.runCommand("debugconfig config " + profile.getName());
context.waitForScreen(ReconfiguringScreen.class);
context.takeScreenshot("server_config");
server.runCommand("debugconfig unconfig " + profile.getId());
waitForWorldTicks(context, 1);
{
enableDebugHud(context);
singleplayer.getClientWorld().waitForChunksRender();
context.takeScreenshot("in_game_overworld", 0);
}
context.setScreen(() -> new GameMenuScreen(true));
context.takeScreenshot("server_game_menu");
context.clickScreenButton("menu.disconnect");
{
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);
}
context.waitForScreen(MultiplayerScreen.class);
context.clickScreenButton("gui.back");
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.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<Path> 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 @@ public class ClientGameTestTest implements FabricClientGameTest {
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);
}
}