Skip to content

Commit

Permalink
Add client game test (#1106)
Browse files Browse the repository at this point in the history
* Add SkyblockerGameTest

* Run in CI

* Use Xvfb

* Use one workflow file

* Always upload screenshots

* Update fabric api to 0.114.0

* Add screenshot comparison

* Make fancy status bars consistent
  • Loading branch information
kevinthegreat1 authored Jan 25, 2025
1 parent f73a04e commit 922313c
Show file tree
Hide file tree
Showing 11 changed files with 221 additions and 56 deletions.
27 changes: 27 additions & 0 deletions .github/workflows/beta.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,30 @@ jobs:
with:
name: ${{ steps.fname.outputs.result }}
path: build/libs/


client_game_test:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
distribution: 'microsoft'
java-version: '21'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3
with:
validate-wrappers: true

- name: Run client gametest with Xvfb
uses: modmuss50/xvfb-action@v1
with:
run: ./gradlew runClientGametest
- name: Upload test screenshots
uses: actions/upload-artifact@v4
if: always()
with:
name: Test Screenshots
path: run/screenshots
8 changes: 8 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,14 @@ loom {
mixin {
useLegacyMixinAp = false
}

runs {
clientGametest {
inherit client
name "Client Game Test"
vmArg "-Dfabric.client.gametest"
}
}
}

base {
Expand Down
4 changes: 2 additions & 2 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ org.gradle.parallel=true
# Fabric Properties (https://fabricmc.net/versions.html)
## 1.21.4
minecraft_version=1.21.4
yarn_mappings=1.21.4+build.1
yarn_mappings=1.21.4+build.2
loader_version=0.16.9

#Fabric api
## 1.21.4
fabric_api_version=0.111.0+1.21.4
fabric_api_version=0.115.0+1.21.4

# Minecraft Mods
## YACL (https://github.com/isXander/YetAnotherConfigLib)
Expand Down
60 changes: 60 additions & 0 deletions src/main/java/de/hysky/skyblocker/SkyblockerGameTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package de.hysky.skyblocker;

import de.hysky.skyblocker.debug.SnapshotDebug;
import de.hysky.skyblocker.skyblock.fancybars.FancyStatusBars;
import it.unimi.dsi.fastutil.Pair;
import net.fabricmc.fabric.api.client.gametest.v1.ClientGameTestContext;
import net.fabricmc.fabric.api.client.gametest.v1.FabricClientGameTest;
import net.fabricmc.fabric.api.client.gametest.v1.TestScreenshotComparisonOptions;
import net.fabricmc.fabric.api.client.gametest.v1.TestSingleplayerContext;
import net.minecraft.client.gui.screen.world.WorldCreator;
import net.minecraft.registry.RegistryKeys;
import net.minecraft.world.gen.WorldPresets;

@SuppressWarnings("UnstableApiUsage")
public class SkyblockerGameTest implements FabricClientGameTest {
@Override
public void runTest(ClientGameTestContext context) {
try (TestSingleplayerContext singleplayer = context.worldBuilder().adjustSettings(worldCreator -> {
worldCreator.setWorldType(new WorldCreator.WorldType(worldCreator.getGeneratorOptionsHolder().getCombinedRegistryManager().getOrThrow(RegistryKeys.WORLD_PRESET).getOrThrow(WorldPresets.DEFAULT)));
worldCreator.setSeed(String.valueOf(SnapshotDebug.AARON_WORLD_SEED));
}).create()) {
// Set up the world
singleplayer.getServer().runCommand("/fill 180 63 -13 184 67 -17 air");
singleplayer.getServer().runCommand("/setblock 175 66 -4 minecraft:barrier");
singleplayer.getServer().runCommand("/tp @a 175 67 -4");

context.runOnClient(client -> {
assert client.player != null;
client.player.setYaw(180);
client.player.setPitch(20);
});

// Save the current fancy status bars config and reset it to default
var config = context.computeOnClient(client -> {
var curConfig = FancyStatusBars.statusBars.entrySet().stream().map(e -> Pair.of(e.getKey(), e.getValue().toJson())).toList();

int[] counts = new int[7];
FancyStatusBars.statusBars.forEach((type, bar) -> {
bar.anchor = type.getDefaultAnchor();
bar.gridY = type.getDefaultGridY();
bar.gridX = counts[type.getDefaultAnchor().ordinal()]++;
});
FancyStatusBars.placeBarsInPositioner();
FancyStatusBars.updatePositions();
return curConfig;
});

// Take a screenshot and compare it
singleplayer.getClientWorld().waitForChunksRender();
context.assertScreenshotEquals(TestScreenshotComparisonOptions.of("skyblocker_render").saveWithFileName("skyblocker_render"));

// Restore the fancy status bars config
context.runOnClient(client -> {
config.forEach(pair -> FancyStatusBars.statusBars.get(pair.key()).loadFromJson(pair.value()));
FancyStatusBars.placeBarsInPositioner();
FancyStatusBars.updatePositions();
});
}
}
}
2 changes: 1 addition & 1 deletion src/main/java/de/hysky/skyblocker/debug/Debug.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public class Debug {
private static boolean keyDown = false;

public static boolean debugEnabled() {
return DEBUG_ENABLED || FabricLoader.getInstance().isDevelopmentEnvironment();
return DEBUG_ENABLED || FabricLoader.getInstance().isDevelopmentEnvironment() || SnapshotDebug.isInSnapshot();
}

public static boolean webSocketDebug() {
Expand Down
8 changes: 4 additions & 4 deletions src/main/java/de/hysky/skyblocker/debug/SnapshotDebug.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ public class SnapshotDebug {
private static final float[] RED = { 1.0f, 0.0f, 0.0f };
private static final float ALPHA = 0.5f;
private static final float LINE_WIDTH = 8f;
private static final long AARON_WORLD_SEED = 5629719634239627355L;
public static final long AARON_WORLD_SEED = 5629719634239627355L;

private static boolean isInSnapshot() {
public static boolean isInSnapshot() {
return !SharedConstants.getGameVersion().isStable();
}

static void init() {
if (isInSnapshot()) {
if (Debug.debugEnabled()) {
WorldRenderEvents.AFTER_TRANSLUCENT.register(SnapshotDebug::renderTest);
}
}
Expand All @@ -32,7 +32,7 @@ private static void renderTest(WorldRenderContext wrc) {
RenderHelper.renderLinesFromPoints(wrc, new Vec3d[] { new Vec3d(173, 66, -7.5), new Vec3d(178, 66, -7.5) }, RED, ALPHA, LINE_WIDTH, false);
RenderHelper.renderQuad(wrc, new Vec3d[] { new Vec3d(183, 66, -16), new Vec3d(183, 63, -16), new Vec3d(183, 63, -14), new Vec3d(183, 66, -14) }, RED, ALPHA, false);
RenderHelper.renderText(wrc, Text.of("Skyblocker on " + SharedConstants.getGameVersion().getName() + "!"), new Vec3d(175.5, 67.5, -7.5), false);
} else {
} else if (isInSnapshot()) {
RenderHelper.renderFilledWithBeaconBeam(wrc, new BlockPos(-3, 63, 5), RED, ALPHA, true);
RenderHelper.renderOutline(wrc, new BlockPos(-3, 63, 5), RED, 5, true); // Use waypoint default line width
RenderHelper.renderLinesFromPoints(wrc, new Vec3d[] { new Vec3d(-2, 65, 6.5), new Vec3d(3, 65, 6.5) }, RED, ALPHA, LINE_WIDTH, false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.*;
import java.util.EnumMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

public class BarPositioner {

private final Map<BarAnchor, LinkedList<LinkedList<StatusBar>>> map = new HashMap<>(BarAnchor.values().length);
private final Map<BarAnchor, LinkedList<LinkedList<StatusBar>>> map = new EnumMap<>(BarAnchor.class);

public BarPositioner() {
for (BarAnchor value : BarAnchor.values()) {
Expand Down Expand Up @@ -145,6 +148,10 @@ public boolean hasNeighbor(@NotNull BarAnchor barAnchor, int row, int x, boolean
}
}

public void clear() {
map.replaceAll((barAnchor, rows) -> new LinkedList<>());
}


public enum BarAnchor {
HOTBAR_LEFT(true, false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,18 @@
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.ScreenPos;
import net.minecraft.text.Text;
import net.minecraft.util.Identifier;
import net.minecraft.util.math.MathHelper;
import org.jetbrains.annotations.VisibleForTesting;
import org.lwjgl.glfw.GLFW;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.awt.*;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.List;
import java.util.*;
import java.util.concurrent.CompletableFuture;

Expand All @@ -40,58 +37,43 @@ public class FancyStatusBars {
private final StatusBarTracker statusBarTracker = SkyblockerMod.getInstance().statusBarTracker;

public static BarPositioner barPositioner = new BarPositioner();
public static Map<String, StatusBar> statusBars = new HashMap<>();
public static Map<StatusBarType, StatusBar> statusBars = new EnumMap<>(StatusBarType.class);

public static boolean isHealthFancyBarVisible() {
StatusBar health = statusBars.get("health");
StatusBar health = statusBars.get(StatusBarType.HEALTH);
return health.anchor != null || health.inMouse;
}

public static boolean isExperienceFancyBarVisible() {
StatusBar experience = statusBars.get("experience");
StatusBar experience = statusBars.get(StatusBarType.EXPERIENCE);
return experience.anchor != null || experience.inMouse;
}

@SuppressWarnings("deprecation")
@Init
public static void init() {
statusBars.put("health", new StatusBar(Identifier.of(SkyblockerMod.NAMESPACE, "bars/icons/health"),
new Color[]{new Color(255, 0, 0), new Color(255, 220, 0)},
true, new Color(255, 85, 85), Text.translatable("skyblocker.bars.config.health")));
statusBars.put("intelligence", new StatusBar(Identifier.of(SkyblockerMod.NAMESPACE, "bars/icons/intelligence"),
new Color[]{new Color(0, 255, 255), new Color(180, 0, 255)},
true, new Color(85, 255, 255), Text.translatable("skyblocker.bars.config.intelligence")));
statusBars.put("defense", new StatusBar(Identifier.of(SkyblockerMod.NAMESPACE, "bars/icons/defense"),
new Color[]{new Color(255, 255, 255)},
false, new Color(185, 185, 185), Text.translatable("skyblocker.bars.config.defense")));
statusBars.put("experience", new StatusBar(Identifier.of(SkyblockerMod.NAMESPACE, "bars/icons/experience"),
new Color[]{new Color(100, 230, 70)},
false, new Color(128, 255, 32), Text.translatable("skyblocker.bars.config.experience")));
statusBars.put("speed", new StatusBar(Identifier.of(SkyblockerMod.NAMESPACE, "bars/icons/speed"),
new Color[]{new Color(255, 255, 255)},
false, new Color(185, 185, 185), Text.translatable("skyblocker.bars.config.speed")));
statusBars.put(StatusBarType.HEALTH, StatusBarType.HEALTH.newStatusBar());
statusBars.put(StatusBarType.INTELLIGENCE, StatusBarType.INTELLIGENCE.newStatusBar());
statusBars.put(StatusBarType.DEFENSE, StatusBarType.DEFENSE.newStatusBar());
statusBars.put(StatusBarType.EXPERIENCE, StatusBarType.EXPERIENCE.newStatusBar());
statusBars.put(StatusBarType.SPEED, StatusBarType.SPEED.newStatusBar());

// Fetch from old status bar config
int[] counts = new int[3]; // counts for RIGHT, LAYER1, LAYER2
StatusBar health = statusBars.get("health");

UIAndVisualsConfig.LegacyBarPositions barPositions = SkyblockerConfigManager.get().uiAndVisuals.bars.barPositions;
initBarPosition(health, counts, barPositions.healthBarPosition);
StatusBar intelligence = statusBars.get("intelligence");
initBarPosition(intelligence, counts, barPositions.manaBarPosition);
StatusBar defense = statusBars.get("defense");
initBarPosition(defense, counts, barPositions.defenceBarPosition);
StatusBar experience = statusBars.get("experience");
initBarPosition(experience, counts, barPositions.experienceBarPosition);
StatusBar speed = statusBars.get("speed");
initBarPosition(speed, counts, UIAndVisualsConfig.LegacyBarPosition.RIGHT);
initBarPosition(statusBars.get(StatusBarType.HEALTH), counts, barPositions.healthBarPosition);
initBarPosition(statusBars.get(StatusBarType.INTELLIGENCE), counts, barPositions.manaBarPosition);
initBarPosition(statusBars.get(StatusBarType.DEFENSE), counts, barPositions.defenceBarPosition);
initBarPosition(statusBars.get(StatusBarType.EXPERIENCE), counts, barPositions.experienceBarPosition);
initBarPosition(statusBars.get(StatusBarType.SPEED), counts, UIAndVisualsConfig.LegacyBarPosition.RIGHT);

CompletableFuture.supplyAsync(FancyStatusBars::loadBarConfig).thenAccept(object -> {
if (object != null) {
for (String s : object.keySet()) {
if (statusBars.containsKey(s)) {
StatusBarType type = StatusBarType.from(s);
if (statusBars.containsKey(type)) {
try {
statusBars.get(s).loadFromJson(object.get(s).getAsJsonObject());
statusBars.get(type).loadFromJson(object.get(s).getAsJsonObject());
} catch (Exception e) {
LOGGER.error("[Skyblocker] Failed to load {} status bar", s, e);
}
Expand Down Expand Up @@ -145,13 +127,13 @@ private static void initBarPosition(StatusBar bar, int[] counts, UIAndVisualsCon

private static boolean configLoaded = false;

private static void placeBarsInPositioner() {
List<StatusBar> original = statusBars.values().stream().toList();

@VisibleForTesting
public static void placeBarsInPositioner() {
barPositioner.clear();
for (BarPositioner.BarAnchor barAnchor : BarPositioner.BarAnchor.allAnchors()) {
List<StatusBar> barList = new ArrayList<>(original.stream().filter(bar -> bar.anchor == barAnchor).toList());
List<StatusBar> barList = statusBars.values().stream().filter(bar -> bar.anchor == barAnchor)
.sorted(Comparator.<StatusBar>comparingInt(bar -> bar.gridY).thenComparingInt(bar -> bar.gridX)).toList();
if (barList.isEmpty()) continue;
barList.sort((a, b) -> a.gridY == b.gridY ? Integer.compare(a.gridX, b.gridX) : Integer.compare(a.gridY, b.gridY));

int y = -1;
int rowNum = -1;
Expand Down Expand Up @@ -179,7 +161,7 @@ public static JsonObject loadBarConfig() {

public static void saveBarConfig() {
JsonObject output = new JsonObject();
statusBars.forEach((s, statusBar) -> output.add(s, statusBar.toJson()));
statusBars.forEach((s, statusBar) -> output.add(s.asString(), statusBar.toJson()));
try (BufferedWriter writer = Files.newBufferedWriter(FILE)) {
SkyblockerMod.GSON.toJson(output, writer);
LOGGER.info("[Skyblocker] Saved status bars config");
Expand Down Expand Up @@ -315,16 +297,16 @@ public boolean render(DrawContext context, int scaledWidth, int scaledHeight) {
for (StatusBar statusBar : barCollection) {
if (statusBar.anchor != null) statusBar.render(context, -1, -1, client.getRenderTickCounter().getLastFrameDuration());
}
StatusBarTracker.Resource health = statusBarTracker.getHealth();
statusBars.get("health").updateValues(health.value() / (float) health.max(), health.overflow() / (float) health.max(), health.value());

StatusBarTracker.Resource health = statusBarTracker.getHealth();
statusBars.get(StatusBarType.HEALTH).updateValues(health.value() / (float) health.max(), health.overflow() / (float) health.max(), health.value());
StatusBarTracker.Resource intelligence = statusBarTracker.getMana();
statusBars.get("intelligence").updateValues(intelligence.value() / (float) intelligence.max(), intelligence.overflow() / (float) intelligence.max(), intelligence.value());
statusBars.get(StatusBarType.INTELLIGENCE).updateValues(intelligence.value() / (float) intelligence.max(), intelligence.overflow() / (float) intelligence.max(), intelligence.value());
int defense = statusBarTracker.getDefense();
statusBars.get("defense").updateValues(defense / (defense + 100.f), 0, defense);
statusBars.get(StatusBarType.DEFENSE).updateValues(defense / (defense + 100.f), 0, defense);
StatusBarTracker.Resource speed = statusBarTracker.getSpeed();
statusBars.get("speed").updateValues(speed.value() / (float) speed.max(), 0, speed.value());
statusBars.get("experience").updateValues(player.experienceProgress, 0, player.experienceLevel);
statusBars.get(StatusBarType.SPEED).updateValues(speed.value() / (float) speed.max(), 0, speed.value());
statusBars.get(StatusBarType.EXPERIENCE).updateValues(player.experienceProgress, 0, player.experienceLevel);
return true;
}
}
Loading

0 comments on commit 922313c

Please sign in to comment.