mirror of
https://github.com/FabricMC/fabric.git
synced 2025-04-21 03:10:54 -04:00
Client gametest screenshot changes (#4329)
* Instant screenshots, with counter and more configurability * Force consistent window size across all systems and stretch framebuffer to fit the physical window * Docs * Wait ticks appropriately for the screenshots in the gametest test * Should -> must * Fix window resizing for different display scales * Fix framebuffer size not being changed at all * Add test for window resizing
This commit is contained in:
parent
f371ccb95a
commit
1f6471e67c
16 changed files with 626 additions and 26 deletions
fabric-client-gametest-api-v1/src
client
java/net/fabricmc/fabric
api/client/gametest/v1
impl/client/gametest
ClientGameTestContextImpl.javaClientGameTestImpl.javaFabricClientGameTestRunner.javaTestInputImpl.javaTestScreenshotOptionsImpl.javaWindowHooks.java
mixin/client/gametest
resources
testmodClient/java/net/fabricmc/fabric/test/client/gametest
|
@ -111,19 +111,20 @@ public interface ClientGameTestContext {
|
|||
boolean tryClickScreenButton(String translationKey);
|
||||
|
||||
/**
|
||||
* Takes a screenshot after waiting 1 tick (for a frame to render) and saves it in the screenshots directory.
|
||||
* Takes a screenshot and saves it in the screenshots directory.
|
||||
*
|
||||
* @param name The name of the screenshot
|
||||
* @return The {@link Path} to the screenshot
|
||||
*/
|
||||
Path takeScreenshot(String name);
|
||||
|
||||
/**
|
||||
* Takes a screnshot after waiting {@code delay} ticks and saves it in the screenshots directory.
|
||||
* Takes a screenshot with the given options.
|
||||
*
|
||||
* @param name The name of the screenshot
|
||||
* @param delay The delay in ticks before taking the screenshot
|
||||
* @param options The {@link TestScreenshotOptions} to take the screenshot with
|
||||
* @return The {@link Path} to the screenshot
|
||||
*/
|
||||
Path takeScreenshot(String name, int delay);
|
||||
Path takeScreenshot(TestScreenshotOptions options);
|
||||
|
||||
/**
|
||||
* Gets the input handler used to simulate inputs to the client.
|
||||
|
|
|
@ -325,4 +325,14 @@ public interface TestInput {
|
|||
* @see #setCursorPos(double, double)
|
||||
*/
|
||||
void moveCursor(double deltaX, double deltaY);
|
||||
|
||||
/**
|
||||
* Resizes the window to match the given size. Also attempts to resize the physical window, but whether the physical
|
||||
* window was successfully resized or not, the window size accessible by the game will always be changed to the
|
||||
* value specified, causing widget layouts and screenshots to work as expected.
|
||||
*
|
||||
* @param width The new window width
|
||||
* @param height The new window height
|
||||
*/
|
||||
void resizeWindow(int width, int height);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* 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 com.google.common.base.Preconditions;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
|
||||
import net.fabricmc.fabric.impl.client.gametest.TestScreenshotOptionsImpl;
|
||||
|
||||
/**
|
||||
* Options to customize a screenshot.
|
||||
*/
|
||||
@ApiStatus.NonExtendable
|
||||
public interface TestScreenshotOptions {
|
||||
/**
|
||||
* Creates a {@link TestScreenshotOptions} with the given screenshot name.
|
||||
*
|
||||
* @param name The name of the screenshot
|
||||
* @return The new screenshot options instance
|
||||
*/
|
||||
static TestScreenshotOptions of(String name) {
|
||||
Preconditions.checkNotNull(name, "name");
|
||||
return new TestScreenshotOptionsImpl(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* By default, screenshot file names will be prefixed by a counter so that the screenshots appear in sequence in the
|
||||
* screenshots directory. Use this method to disable this behavior.
|
||||
*
|
||||
* @return This screenshot options instance
|
||||
*/
|
||||
TestScreenshotOptions disableCounterPrefix();
|
||||
|
||||
/**
|
||||
* Changes the tick delta to take this screenshot with. Tick delta controls interpolation between the previous tick and the
|
||||
* current tick to make objects appear to move more smoothly when there are multiple frames in a tick. Defaults to
|
||||
* {@code 1}, which renders all objects as their appear in the current tick.
|
||||
*
|
||||
* @param tickDelta The tick delta to take this screenshot with
|
||||
* @return This screenshot options instance
|
||||
*/
|
||||
TestScreenshotOptions withTickDelta(float tickDelta);
|
||||
|
||||
/**
|
||||
* Changes the resolution of the screenshot, which defaults to the resolution of the Minecraft window.
|
||||
*
|
||||
* @param width The width of the screenshot
|
||||
* @param height The height of the screenshot
|
||||
* @return This screenshot options instance
|
||||
*/
|
||||
TestScreenshotOptions withSize(int width, int height);
|
||||
|
||||
/**
|
||||
* Changes the directory in which this screenshot is saved, which defaults to the {@code screenshots} directory in
|
||||
* the game's run directory.
|
||||
*
|
||||
* @param destinationDir The directory in which to save the screenshot
|
||||
* @return This screenshot options instance
|
||||
*/
|
||||
TestScreenshotOptions withDestinationDir(Path destinationDir);
|
||||
}
|
|
@ -16,9 +16,12 @@
|
|||
|
||||
package net.fabricmc.fabric.impl.client.gametest;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.Supplier;
|
||||
|
@ -41,6 +44,7 @@ import net.minecraft.client.gui.widget.Widget;
|
|||
import net.minecraft.client.option.CloudRenderMode;
|
||||
import net.minecraft.client.option.GameOptions;
|
||||
import net.minecraft.client.option.SimpleOption;
|
||||
import net.minecraft.client.texture.NativeImage;
|
||||
import net.minecraft.client.tutorial.TutorialStep;
|
||||
import net.minecraft.client.util.ScreenshotRecorder;
|
||||
import net.minecraft.sound.SoundCategory;
|
||||
|
@ -48,9 +52,11 @@ 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.TestScreenshotOptions;
|
||||
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.RenderTickCounterConstantAccessor;
|
||||
import net.fabricmc.fabric.mixin.client.gametest.ScreenAccessor;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
|
||||
|
@ -262,22 +268,56 @@ public final class ClientGameTestContextImpl implements ClientGameTestContext {
|
|||
public Path takeScreenshot(String name) {
|
||||
ThreadingImpl.checkOnGametestThread("takeScreenshot");
|
||||
Preconditions.checkNotNull(name, "name");
|
||||
return takeScreenshot(name, 1);
|
||||
return takeScreenshot(TestScreenshotOptions.of(name));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Path takeScreenshot(String name, int delay) {
|
||||
public Path takeScreenshot(TestScreenshotOptions options) {
|
||||
ThreadingImpl.checkOnGametestThread("takeScreenshot");
|
||||
Preconditions.checkNotNull(name, "name");
|
||||
Preconditions.checkArgument(delay >= 0, "delay cannot be negative");
|
||||
Preconditions.checkNotNull(options, "options");
|
||||
|
||||
waitTicks(delay);
|
||||
runOnClient(client -> {
|
||||
ScreenshotRecorder.saveScreenshot(FabricLoader.getInstance().getGameDir().toFile(), name + ".png", client.getFramebuffer(), (message) -> {
|
||||
});
|
||||
TestScreenshotOptionsImpl optionsImpl = (TestScreenshotOptionsImpl) options;
|
||||
return computeOnClient(client -> {
|
||||
int prevWidth = client.getWindow().getFramebufferWidth();
|
||||
int prevHeight = client.getWindow().getFramebufferHeight();
|
||||
|
||||
if (optionsImpl.size != null) {
|
||||
client.getWindow().setFramebufferWidth(optionsImpl.size.x);
|
||||
client.getWindow().setFramebufferHeight(optionsImpl.size.y);
|
||||
client.getFramebuffer().resize(optionsImpl.size.x, optionsImpl.size.y);
|
||||
}
|
||||
|
||||
try {
|
||||
client.gameRenderer.render(RenderTickCounterConstantAccessor.create(optionsImpl.tickDelta), true);
|
||||
|
||||
// The vanilla panorama screenshot code has a Thread.sleep(10) here, is this needed?
|
||||
|
||||
Path destinationDir = Objects.requireNonNullElseGet(optionsImpl.destinationDir, () -> FabricLoader.getInstance().getGameDir().resolve("screenshots"));
|
||||
|
||||
try {
|
||||
Files.createDirectories(destinationDir);
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("Failed to create screenshots directory", e);
|
||||
}
|
||||
|
||||
String counterPrefix = optionsImpl.counterPrefix ? "%04d_".formatted(ClientGameTestImpl.screenshotCounter++) : "";
|
||||
Path screenshotFile = destinationDir.resolve(counterPrefix + optionsImpl.name + ".png");
|
||||
|
||||
try (NativeImage screenshot = ScreenshotRecorder.takeScreenshot(client.getFramebuffer())) {
|
||||
screenshot.writeTo(screenshotFile);
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("Failed to write screenshot file", e);
|
||||
}
|
||||
|
||||
return screenshotFile;
|
||||
} finally {
|
||||
if (optionsImpl.size != null) {
|
||||
client.getWindow().setFramebufferWidth(prevWidth);
|
||||
client.getWindow().setFramebufferHeight(prevHeight);
|
||||
client.getFramebuffer().resize(prevWidth, prevHeight);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return FabricLoader.getInstance().getGameDir().resolve("screenshots").resolve(name + ".png");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -31,6 +31,7 @@ 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");
|
||||
public static int screenshotCounter = 0;
|
||||
|
||||
private ClientGameTestImpl() {
|
||||
}
|
||||
|
|
|
@ -38,17 +38,24 @@ public class FabricClientGameTestRunner {
|
|||
ClientGameTestContextImpl context = new ClientGameTestContextImpl();
|
||||
|
||||
for (FabricClientGameTest gameTest : gameTests) {
|
||||
context.restoreDefaultGameOptions();
|
||||
setupInitialGameTestState(context);
|
||||
|
||||
gameTest.runTest(context);
|
||||
|
||||
context.getInput().clearKeysDown();
|
||||
checkFinalGameTestState(context, gameTest.getClass().getName());
|
||||
setupAndCheckFinalGameTestState(context, gameTest.getClass().getName());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void checkFinalGameTestState(ClientGameTestContext context, String testClassName) {
|
||||
private static void setupInitialGameTestState(ClientGameTestContext context) {
|
||||
context.restoreDefaultGameOptions();
|
||||
}
|
||||
|
||||
private static void setupAndCheckFinalGameTestState(ClientGameTestContextImpl context, String testClassName) {
|
||||
context.getInput().clearKeysDown();
|
||||
context.runOnClient(client -> ((WindowHooks) (Object) client.getWindow()).fabric_resetSize());
|
||||
context.getInput().setCursorPos(context.computeOnClient(client -> client.getWindow().getWidth()) * 0.5, context.computeOnClient(client -> client.getWindow().getHeight()) * 0.5);
|
||||
|
||||
if (ThreadingImpl.isServerRunning) {
|
||||
throw new AssertionError("Client gametest %s finished while a server is still running".formatted(testClassName));
|
||||
}
|
||||
|
|
|
@ -321,6 +321,15 @@ public final class TestInputImpl implements TestInput {
|
|||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resizeWindow(int width, int height) {
|
||||
ThreadingImpl.checkOnGametestThread("resizeWindow");
|
||||
Preconditions.checkArgument(width > 0, "width must be positive");
|
||||
Preconditions.checkArgument(height > 0, "height must be positive");
|
||||
|
||||
context.runOnClient(client -> ((WindowHooks) (Object) client.getWindow()).fabric_resize(width, height));
|
||||
}
|
||||
|
||||
private static InputUtil.Key getBoundKey(KeyBinding keyBinding, String action) {
|
||||
InputUtil.Key boundKey = ((KeyBindingAccessor) keyBinding).getBoundKey();
|
||||
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.fabricmc.fabric.impl.client.gametest;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.joml.Vector2i;
|
||||
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.TestScreenshotOptions;
|
||||
|
||||
public class TestScreenshotOptionsImpl implements TestScreenshotOptions {
|
||||
public final String name;
|
||||
public boolean counterPrefix = true;
|
||||
public float tickDelta = 1;
|
||||
@Nullable
|
||||
public Vector2i size;
|
||||
@Nullable
|
||||
public Path destinationDir;
|
||||
|
||||
public TestScreenshotOptionsImpl(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TestScreenshotOptions disableCounterPrefix() {
|
||||
counterPrefix = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TestScreenshotOptions withTickDelta(float tickDelta) {
|
||||
Preconditions.checkArgument(tickDelta >= 0 && tickDelta <= 1, "tickDelta must be between 0 and 1");
|
||||
|
||||
this.tickDelta = tickDelta;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TestScreenshotOptions withSize(int width, int height) {
|
||||
Preconditions.checkArgument(width > 0, "width must be positive");
|
||||
Preconditions.checkArgument(height > 0, "height must be positive");
|
||||
|
||||
this.size = new Vector2i(width, height);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TestScreenshotOptions withDestinationDir(Path destinationDir) {
|
||||
Preconditions.checkNotNull(destinationDir, "destinationDir");
|
||||
|
||||
this.destinationDir = destinationDir;
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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;
|
||||
|
||||
public interface WindowHooks {
|
||||
int fabric_getRealWidth();
|
||||
int fabric_getRealHeight();
|
||||
int fabric_getRealFramebufferWidth();
|
||||
int fabric_getRealFramebufferHeight();
|
||||
void fabric_resetSize();
|
||||
void fabric_resize(int width, int height);
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.fabricmc.fabric.mixin.client.gametest;
|
||||
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.ModifyVariable;
|
||||
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
import net.minecraft.client.gl.Framebuffer;
|
||||
import net.minecraft.client.util.Window;
|
||||
|
||||
import net.fabricmc.fabric.impl.client.gametest.WindowHooks;
|
||||
|
||||
@Mixin(Framebuffer.class)
|
||||
public class FramebufferMixin {
|
||||
@ModifyVariable(method = {"draw", "drawInternal"}, at = @At("HEAD"), ordinal = 0, argsOnly = true)
|
||||
private int modifyWidth(int width) {
|
||||
Window window = MinecraftClient.getInstance().getWindow();
|
||||
|
||||
if ((Object) this == MinecraftClient.getInstance().getFramebuffer() && width == window.getFramebufferWidth()) {
|
||||
return ((WindowHooks) (Object) window).fabric_getRealFramebufferWidth();
|
||||
}
|
||||
|
||||
return width;
|
||||
}
|
||||
|
||||
@ModifyVariable(method = {"draw", "drawInternal"}, at = @At("HEAD"), ordinal = 1, argsOnly = true)
|
||||
private int modifyHeight(int height) {
|
||||
Window window = MinecraftClient.getInstance().getWindow();
|
||||
|
||||
if ((Object) this == MinecraftClient.getInstance().getFramebuffer() && height == window.getFramebufferHeight()) {
|
||||
return ((WindowHooks) (Object) window).fabric_getRealFramebufferHeight();
|
||||
}
|
||||
|
||||
return height;
|
||||
}
|
||||
}
|
|
@ -17,9 +17,11 @@
|
|||
package net.fabricmc.fabric.mixin.client.gametest;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
|
||||
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.spongepowered.asm.mixin.Final;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.Unique;
|
||||
|
@ -31,12 +33,14 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
|||
import net.minecraft.client.MinecraftClient;
|
||||
import net.minecraft.client.gui.screen.Overlay;
|
||||
import net.minecraft.client.gui.screen.Screen;
|
||||
import net.minecraft.client.util.Window;
|
||||
import net.minecraft.resource.ResourcePackManager;
|
||||
import net.minecraft.server.SaveLoader;
|
||||
import net.minecraft.world.level.storage.LevelStorage;
|
||||
|
||||
import net.fabricmc.fabric.impl.client.gametest.FabricClientGameTestRunner;
|
||||
import net.fabricmc.fabric.impl.client.gametest.ThreadingImpl;
|
||||
import net.fabricmc.fabric.impl.client.gametest.WindowHooks;
|
||||
|
||||
@Mixin(MinecraftClient.class)
|
||||
public class MinecraftClientMixin {
|
||||
|
@ -49,6 +53,10 @@ public class MinecraftClientMixin {
|
|||
@Nullable
|
||||
private Overlay overlay;
|
||||
|
||||
@Shadow
|
||||
@Final
|
||||
private Window window;
|
||||
|
||||
@WrapMethod(method = "run")
|
||||
private void onRun(Operation<Void> original) throws Throwable {
|
||||
if (ThreadingImpl.isClientRunning) {
|
||||
|
@ -162,6 +170,12 @@ public class MinecraftClientMixin {
|
|||
);
|
||||
}
|
||||
|
||||
@ModifyExpressionValue(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/util/Window;hasZeroWidthOrHeight()Z"))
|
||||
private boolean hasZeroRealWidthOrHeight(boolean original) {
|
||||
WindowHooks windowHooks = (WindowHooks) (Object) window;
|
||||
return windowHooks.fabric_getRealFramebufferWidth() == 0 || windowHooks.fabric_getRealFramebufferHeight() == 0;
|
||||
}
|
||||
|
||||
@Unique
|
||||
private static void deregisterClient() {
|
||||
if (ThreadingImpl.isClientRunning) {
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* 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.ModifyExpressionValue;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
|
||||
import net.minecraft.client.util.MonitorTracker;
|
||||
import net.minecraft.client.util.Window;
|
||||
|
||||
import net.fabricmc.fabric.impl.client.gametest.WindowHooks;
|
||||
|
||||
@Mixin(MonitorTracker.class)
|
||||
public class MonitorTrackerMixin {
|
||||
@ModifyExpressionValue(method = "getMonitor(Lnet/minecraft/client/util/Window;)Lnet/minecraft/client/util/Monitor;", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/util/Window;getWidth()I"))
|
||||
private int getRealWidth(int original, Window window) {
|
||||
return ((WindowHooks) (Object) window).fabric_getRealWidth();
|
||||
}
|
||||
|
||||
@ModifyExpressionValue(method = "getMonitor(Lnet/minecraft/client/util/Window;)Lnet/minecraft/client/util/Monitor;", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/util/Window;getHeight()I"))
|
||||
private int getRealHeight(int original, Window window) {
|
||||
return ((WindowHooks) (Object) window).fabric_getRealHeight();
|
||||
}
|
||||
}
|
|
@ -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 org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.gen.Invoker;
|
||||
|
||||
import net.minecraft.client.render.RenderTickCounter;
|
||||
|
||||
@Mixin(RenderTickCounter.Constant.class)
|
||||
public interface RenderTickCounterConstantAccessor {
|
||||
@Invoker("<init>")
|
||||
static RenderTickCounter.Constant create(float constant) {
|
||||
throw new UnsupportedOperationException("Implemented via mixin");
|
||||
}
|
||||
}
|
|
@ -16,17 +16,199 @@
|
|||
|
||||
package net.fabricmc.fabric.mixin.client.gametest;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.spongepowered.asm.mixin.Final;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.Unique;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
import net.minecraft.client.WindowEventHandler;
|
||||
import net.minecraft.client.WindowSettings;
|
||||
import net.minecraft.client.util.Monitor;
|
||||
import net.minecraft.client.util.MonitorTracker;
|
||||
import net.minecraft.client.util.VideoMode;
|
||||
import net.minecraft.client.util.Window;
|
||||
|
||||
import net.fabricmc.fabric.impl.client.gametest.WindowHooks;
|
||||
|
||||
@Mixin(Window.class)
|
||||
public class WindowMixin {
|
||||
@Inject(method = {"onWindowFocusChanged", "onCursorEnterChanged"}, at = @At("HEAD"), cancellable = true)
|
||||
public abstract class WindowMixin implements WindowHooks {
|
||||
@Shadow
|
||||
private int x;
|
||||
@Shadow
|
||||
private int y;
|
||||
@Shadow
|
||||
private int windowedX;
|
||||
@Shadow
|
||||
private int windowedY;
|
||||
@Shadow
|
||||
private int width;
|
||||
@Shadow
|
||||
private int height;
|
||||
@Shadow
|
||||
private int windowedWidth;
|
||||
@Shadow
|
||||
private int windowedHeight;
|
||||
@Shadow
|
||||
private int framebufferWidth;
|
||||
@Shadow
|
||||
private int framebufferHeight;
|
||||
@Shadow
|
||||
private boolean fullscreen;
|
||||
@Shadow
|
||||
@Final
|
||||
private WindowEventHandler eventHandler;
|
||||
@Shadow
|
||||
@Final
|
||||
private MonitorTracker monitorTracker;
|
||||
@Shadow
|
||||
private Optional<VideoMode> fullscreenVideoMode;
|
||||
|
||||
@Shadow
|
||||
protected abstract void updateWindowRegion();
|
||||
|
||||
@Unique
|
||||
private int defaultWidth;
|
||||
@Unique
|
||||
private int defaultHeight;
|
||||
@Unique
|
||||
private int realWidth;
|
||||
@Unique
|
||||
private int realHeight;
|
||||
@Unique
|
||||
private int realFramebufferWidth;
|
||||
@Unique
|
||||
private int realFramebufferHeight;
|
||||
|
||||
@Inject(method = "<init>", at = @At("RETURN"))
|
||||
private void onInit(WindowEventHandler eventHandler, MonitorTracker monitorTracker, WindowSettings settings, @Nullable String fullscreenVideoMode, String title, CallbackInfo ci) {
|
||||
this.defaultWidth = settings.width;
|
||||
this.defaultHeight = settings.height;
|
||||
this.realWidth = this.width;
|
||||
this.realHeight = this.height;
|
||||
this.realFramebufferWidth = this.framebufferWidth;
|
||||
this.realFramebufferHeight = this.framebufferHeight;
|
||||
|
||||
this.width = this.windowedWidth = this.framebufferWidth = defaultWidth;
|
||||
this.height = this.windowedHeight = this.framebufferHeight = defaultHeight;
|
||||
}
|
||||
|
||||
@Inject(method = {"onWindowFocusChanged", "onCursorEnterChanged", "onMinimizeChanged"}, at = @At("HEAD"), cancellable = true)
|
||||
private void cancelEvents(CallbackInfo ci) {
|
||||
ci.cancel();
|
||||
}
|
||||
|
||||
@Inject(method = "onWindowSizeChanged", at = @At("HEAD"), cancellable = true)
|
||||
private void cancelWindowSizeChanged(long window, int width, int height, CallbackInfo ci) {
|
||||
realWidth = width;
|
||||
realHeight = height;
|
||||
ci.cancel();
|
||||
}
|
||||
|
||||
@Inject(method = "onFramebufferSizeChanged", at = @At("HEAD"), cancellable = true)
|
||||
private void cancelFramebufferSizeChanged(long window, int width, int height, CallbackInfo ci) {
|
||||
realFramebufferWidth = width;
|
||||
realFramebufferHeight = height;
|
||||
ci.cancel();
|
||||
}
|
||||
|
||||
@WrapMethod(method = "updateWindowRegion")
|
||||
private void wrapUpdateWindowRegion(Operation<Void> original) {
|
||||
int prevWidth = this.width;
|
||||
int prevHeight = this.height;
|
||||
int prevWindowedWidth = this.windowedWidth;
|
||||
int prevWindowedHeight = this.windowedHeight;
|
||||
|
||||
original.call();
|
||||
|
||||
this.realWidth = this.width;
|
||||
this.realHeight = this.height;
|
||||
|
||||
this.width = prevWidth;
|
||||
this.height = prevHeight;
|
||||
this.windowedWidth = prevWindowedWidth;
|
||||
this.windowedHeight = prevWindowedHeight;
|
||||
}
|
||||
|
||||
@Inject(method = "setWindowedSize", at = @At("HEAD"), cancellable = true)
|
||||
private void setWindowedSize(int width, int height, CallbackInfo ci) {
|
||||
this.fullscreen = false;
|
||||
fabric_resize(width, height);
|
||||
ci.cancel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int fabric_getRealWidth() {
|
||||
return realWidth;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int fabric_getRealHeight() {
|
||||
return realHeight;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int fabric_getRealFramebufferWidth() {
|
||||
return realFramebufferWidth;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int fabric_getRealFramebufferHeight() {
|
||||
return realFramebufferHeight;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fabric_resetSize() {
|
||||
fabric_resize(defaultWidth, defaultHeight);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fabric_resize(int width, int height) {
|
||||
if (width == this.width && width == this.windowedWidth && width == this.framebufferWidth && height == this.height && height == this.windowedHeight && height == this.framebufferHeight) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Move the top left corner of the window so that the window expands/contracts from its center, while also
|
||||
// trying to keep the window within the monitor's bounds
|
||||
Monitor monitor = this.monitorTracker.getMonitor((Window) (Object) this);
|
||||
|
||||
if (monitor != null) {
|
||||
VideoMode videoMode = monitor.findClosestVideoMode(this.fullscreenVideoMode);
|
||||
|
||||
this.x += (this.windowedWidth - width) / 2;
|
||||
this.y += (this.windowedHeight - height) / 2;
|
||||
|
||||
if (this.x + width > monitor.getViewportX() + videoMode.getWidth()) {
|
||||
this.x = monitor.getViewportX() + videoMode.getWidth() - width;
|
||||
}
|
||||
|
||||
if (this.x < monitor.getViewportX()) {
|
||||
this.x = monitor.getViewportX();
|
||||
}
|
||||
|
||||
if (this.y + height > monitor.getViewportY() + videoMode.getHeight()) {
|
||||
this.y = monitor.getViewportY() + videoMode.getHeight() - height;
|
||||
}
|
||||
|
||||
if (this.y < monitor.getViewportY()) {
|
||||
this.y = monitor.getViewportY();
|
||||
}
|
||||
|
||||
this.windowedX = this.x;
|
||||
this.windowedY = this.y;
|
||||
}
|
||||
|
||||
this.width = this.windowedWidth = this.framebufferWidth = width;
|
||||
this.height = this.windowedHeight = this.framebufferHeight = height;
|
||||
|
||||
updateWindowRegion();
|
||||
this.eventHandler.onResolutionChanged();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,6 +26,9 @@
|
|||
"ClientChunkMapAccessor",
|
||||
"ClientWorldAccessor",
|
||||
"CreateWorldScreenAccessor",
|
||||
"CreateWorldScreenMixin"
|
||||
"CreateWorldScreenMixin",
|
||||
"FramebufferMixin",
|
||||
"MonitorTrackerMixin",
|
||||
"RenderTickCounterConstantAccessor"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -16,13 +16,20 @@
|
|||
|
||||
package net.fabricmc.fabric.test.client.gametest;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import com.mojang.authlib.GameProfile;
|
||||
import org.spongepowered.asm.mixin.MixinEnvironment;
|
||||
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
import net.minecraft.client.gl.WindowFramebuffer;
|
||||
import net.minecraft.client.gui.screen.ReconfiguringScreen;
|
||||
import net.minecraft.client.gui.screen.world.WorldCreator;
|
||||
import net.minecraft.client.option.Perspective;
|
||||
import net.minecraft.client.texture.NativeImage;
|
||||
import net.minecraft.client.util.InputUtil;
|
||||
|
||||
import net.fabricmc.fabric.api.client.gametest.v1.ClientGameTestContext;
|
||||
|
@ -37,7 +44,15 @@ public class ClientGameTestTest implements FabricClientGameTest {
|
|||
public void runTest(ClientGameTestContext context) {
|
||||
{
|
||||
waitForTitleScreenFade(context);
|
||||
context.takeScreenshot("title_screen", 0);
|
||||
context.takeScreenshot("title_screen");
|
||||
}
|
||||
|
||||
{
|
||||
testScreenSize(context, WindowFramebuffer.DEFAULT_WIDTH, WindowFramebuffer.DEFAULT_HEIGHT);
|
||||
context.getInput().resizeWindow(1000, 500);
|
||||
context.waitTick();
|
||||
testScreenSize(context, 1000, 500);
|
||||
context.getInput().resizeWindow(WindowFramebuffer.DEFAULT_WIDTH, WindowFramebuffer.DEFAULT_HEIGHT);
|
||||
}
|
||||
|
||||
TestWorldSave spWorldSave;
|
||||
|
@ -48,7 +63,7 @@ public class ClientGameTestTest implements FabricClientGameTest {
|
|||
{
|
||||
enableDebugHud(context);
|
||||
singleplayer.getClientWorld().waitForChunksRender();
|
||||
context.takeScreenshot("in_game_overworld", 0);
|
||||
context.takeScreenshot("in_game_overworld");
|
||||
}
|
||||
|
||||
{
|
||||
|
@ -56,7 +71,8 @@ public class ClientGameTestTest implements FabricClientGameTest {
|
|||
context.waitTick();
|
||||
context.getInput().typeChars("Hello, World!");
|
||||
context.getInput().pressKey(InputUtil.GLFW_KEY_ENTER);
|
||||
context.takeScreenshot("chat_message_sent", 5);
|
||||
context.waitTick(); // wait for the server to receive the chat message
|
||||
context.takeScreenshot("chat_message_sent");
|
||||
}
|
||||
|
||||
MixinEnvironment.getCurrentEnvironment().audit();
|
||||
|
@ -70,6 +86,7 @@ public class ClientGameTestTest implements FabricClientGameTest {
|
|||
|
||||
{
|
||||
context.getInput().pressKey(options -> options.inventoryKey);
|
||||
context.waitTicks(2); // allow the client to process the key press, and then the server to receive the request
|
||||
context.takeScreenshot("in_game_inventory");
|
||||
context.setScreen(() -> null);
|
||||
}
|
||||
|
@ -83,7 +100,7 @@ public class ClientGameTestTest implements FabricClientGameTest {
|
|||
try (TestDedicatedServerContext server = context.worldBuilder().createServer()) {
|
||||
try (TestServerConnection connection = server.connect()) {
|
||||
connection.getClientWorld().waitForChunksRender();
|
||||
context.takeScreenshot("server_in_game", 0);
|
||||
context.takeScreenshot("server_in_game");
|
||||
|
||||
{ // Test that we can enter and exit configuration
|
||||
final GameProfile profile = context.computeOnClient(MinecraftClient::getGameProfile);
|
||||
|
@ -111,4 +128,26 @@ public class ClientGameTestTest implements FabricClientGameTest {
|
|||
private static void setPerspective(ClientGameTestContext context, Perspective perspective) {
|
||||
context.runOnClient(client -> client.options.setPerspective(perspective));
|
||||
}
|
||||
|
||||
private static void testScreenSize(ClientGameTestContext context, int expectedWidth, int expectedHeight) {
|
||||
context.runOnClient(client -> {
|
||||
if (client.getWindow().getWidth() != expectedWidth || client.getWindow().getHeight() != expectedHeight) {
|
||||
throw new AssertionError("Expected window size to be (%d, %d) but was (%d, %d)".formatted(expectedWidth, expectedHeight, client.getWindow().getWidth(), client.getWindow().getHeight()));
|
||||
}
|
||||
|
||||
if (client.getWindow().getFramebufferWidth() != expectedWidth || client.getWindow().getFramebufferHeight() != expectedHeight) {
|
||||
throw new AssertionError("Expected framebuffer size to be (%d, %d) but was (%d, %d)".formatted(expectedWidth, expectedHeight, client.getWindow().getFramebufferWidth(), client.getWindow().getFramebufferHeight()));
|
||||
}
|
||||
});
|
||||
|
||||
Path screenshotPath = context.takeScreenshot("screenshot_size_test");
|
||||
|
||||
try (NativeImage screenshot = NativeImage.read(Files.newInputStream(screenshotPath))) {
|
||||
if (screenshot.getWidth() != expectedWidth || screenshot.getHeight() != expectedHeight) {
|
||||
throw new AssertionError("Expected screenshot size to be (%d, %d) but was (%d, %d)".formatted(expectedWidth, expectedHeight, screenshot.getWidth(), screenshot.getHeight()));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue