diff --git a/.github/workflows/beta.yml b/.github/workflows/beta.yml index 45743aa4e9..d129d3c13c 100644 --- a/.github/workflows/beta.yml +++ b/.github/workflows/beta.yml @@ -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 diff --git a/build.gradle b/build.gradle index 4e2fcc4f0e..b796fa4f52 100644 --- a/build.gradle +++ b/build.gradle @@ -202,6 +202,14 @@ loom { mixin { useLegacyMixinAp = false } + + runs { + clientGametest { + inherit client + name "Client Game Test" + vmArg "-Dfabric.client.gametest" + } + } } base { diff --git a/gradle.properties b/gradle.properties index 3ce546e8e3..39ab47c9f2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -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) diff --git a/src/main/java/de/hysky/skyblocker/SkyblockerGameTest.java b/src/main/java/de/hysky/skyblocker/SkyblockerGameTest.java new file mode 100644 index 0000000000..d3da8969a3 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/SkyblockerGameTest.java @@ -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(); + }); + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/debug/Debug.java b/src/main/java/de/hysky/skyblocker/debug/Debug.java index 2a9f9a602f..cfd0b52818 100644 --- a/src/main/java/de/hysky/skyblocker/debug/Debug.java +++ b/src/main/java/de/hysky/skyblocker/debug/Debug.java @@ -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() { diff --git a/src/main/java/de/hysky/skyblocker/debug/SnapshotDebug.java b/src/main/java/de/hysky/skyblocker/debug/SnapshotDebug.java index 0f70d651f9..ba89d9421a 100644 --- a/src/main/java/de/hysky/skyblocker/debug/SnapshotDebug.java +++ b/src/main/java/de/hysky/skyblocker/debug/SnapshotDebug.java @@ -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); } } @@ -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); diff --git a/src/main/java/de/hysky/skyblocker/skyblock/fancybars/BarPositioner.java b/src/main/java/de/hysky/skyblocker/skyblock/fancybars/BarPositioner.java index 00b09dcfbb..ffb0b66aa6 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/fancybars/BarPositioner.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/fancybars/BarPositioner.java @@ -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>> map = new HashMap<>(BarAnchor.values().length); + private final Map>> map = new EnumMap<>(BarAnchor.class); public BarPositioner() { for (BarAnchor value : BarAnchor.values()) { @@ -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, diff --git a/src/main/java/de/hysky/skyblocker/skyblock/fancybars/FancyStatusBars.java b/src/main/java/de/hysky/skyblocker/skyblock/fancybars/FancyStatusBars.java index cc6a3571dd..adc5d2b295 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/fancybars/FancyStatusBars.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/fancybars/FancyStatusBars.java @@ -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; @@ -40,58 +37,43 @@ public class FancyStatusBars { private final StatusBarTracker statusBarTracker = SkyblockerMod.getInstance().statusBarTracker; public static BarPositioner barPositioner = new BarPositioner(); - public static Map statusBars = new HashMap<>(); + public static Map 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); } @@ -145,13 +127,13 @@ private static void initBarPosition(StatusBar bar, int[] counts, UIAndVisualsCon private static boolean configLoaded = false; - private static void placeBarsInPositioner() { - List original = statusBars.values().stream().toList(); - + @VisibleForTesting + public static void placeBarsInPositioner() { + barPositioner.clear(); for (BarPositioner.BarAnchor barAnchor : BarPositioner.BarAnchor.allAnchors()) { - List barList = new ArrayList<>(original.stream().filter(bar -> bar.anchor == barAnchor).toList()); + List barList = statusBars.values().stream().filter(bar -> bar.anchor == barAnchor) + .sorted(Comparator.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; @@ -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"); @@ -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; } } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/fancybars/StatusBarType.java b/src/main/java/de/hysky/skyblocker/skyblock/fancybars/StatusBarType.java new file mode 100644 index 0000000000..0ab924e17b --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/fancybars/StatusBarType.java @@ -0,0 +1,78 @@ +package de.hysky.skyblocker.skyblock.fancybars; + +import de.hysky.skyblocker.SkyblockerMod; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; +import net.minecraft.util.StringIdentifiable; +import org.jetbrains.annotations.Nullable; + +import java.awt.*; + +public enum StatusBarType implements StringIdentifiable { + HEALTH("health", BarPositioner.BarAnchor.HOTBAR_TOP, 0, new Color[]{new Color(255, 0, 0), new Color(255, 220, 0)}, true, new Color(255, 85, 85), Text.translatable("skyblocker.bars.config.health")), + INTELLIGENCE("intelligence", BarPositioner.BarAnchor.HOTBAR_TOP, 0, new Color[]{new Color(0, 255, 255), new Color(180, 0, 255)}, true, new Color(85, 255, 255), Text.translatable("skyblocker.bars.config.intelligence")), + DEFENSE("defense", BarPositioner.BarAnchor.HOTBAR_RIGHT, 0, new Color[]{new Color(255, 255, 255)}, false, new Color(185, 185, 185), Text.translatable("skyblocker.bars.config.defense")), + EXPERIENCE("experience", BarPositioner.BarAnchor.HOTBAR_TOP, 1, new Color[]{new Color(100, 230, 70)}, false, new Color(128, 255, 32), Text.translatable("skyblocker.bars.config.experience")), + SPEED("speed", BarPositioner.BarAnchor.HOTBAR_RIGHT, 0, new Color[]{new Color(255, 255, 255)}, false, new Color(185, 185, 185), Text.translatable("skyblocker.bars.config.speed")); + + private final String id; + private final BarPositioner.BarAnchor defaultAnchor; + private final int defaultGridY; + private final Color[] colors; + private final boolean hasOverflow; + @Nullable + private final Color textColor; + private final Text name; + + StatusBarType(String id, BarPositioner.BarAnchor defaultAnchor, int defaultGridY, Color[] colors, boolean hasOverflow, @Nullable Color textColor, Text name) { + this.id = id; + this.defaultAnchor = defaultAnchor; + this.defaultGridY = defaultGridY; + this.colors = colors; + this.hasOverflow = hasOverflow; + this.textColor = textColor; + this.name = name; + } + + public static StatusBarType from(String id) { + for (StatusBarType type : values()) { + if (type.id.equals(id)) { + return type; + } + } + throw new IllegalArgumentException("Unknown status bar type: " + id); + } + + @Override + public String asString() { + return id; + } + + public BarPositioner.BarAnchor getDefaultAnchor() { + return defaultAnchor; + } + + public int getDefaultGridY() { + return defaultGridY; + } + + public Color[] getColors() { + return colors; + } + + public boolean hasOverflow() { + return hasOverflow; + } + + public @Nullable Color getTextColor() { + return textColor; + } + + public Text getName() { + return name; + } + + public StatusBar newStatusBar() { + return new StatusBar(Identifier.of(SkyblockerMod.NAMESPACE, "bars/icons/" + id), colors, hasOverflow, textColor, name); + } +} diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index f94d3b37df..431e0605ea 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -22,6 +22,9 @@ "client": [ "de.hysky.skyblocker.SkyblockerMod" ], + "fabric-client-gametest": [ + "de.hysky.skyblocker.SkyblockerGameTest" + ], "modmenu": [ "de.hysky.skyblocker.compatibility.modmenu.ModMenuEntry" ], diff --git a/src/main/resources/templates/skyblocker_render.png b/src/main/resources/templates/skyblocker_render.png new file mode 100644 index 0000000000..d603a5775e Binary files /dev/null and b/src/main/resources/templates/skyblocker_render.png differ