Client gametest screenshot changes ()

* 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:
Joseph Burton 2024-12-30 13:10:23 +00:00 committed by GitHub
parent f371ccb95a
commit 1f6471e67c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 626 additions and 26 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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) {

View file

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

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

View file

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

View file

@ -26,6 +26,9 @@
"ClientChunkMapAccessor",
"ClientWorldAccessor",
"CreateWorldScreenAccessor",
"CreateWorldScreenMixin"
"CreateWorldScreenMixin",
"FramebufferMixin",
"MonitorTrackerMixin",
"RenderTickCounterConstantAccessor"
]
}

View file

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