diff --git a/gradle.properties b/gradle.properties index 3bc92ec07b..8171cbed31 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ loader_version=0.16.9 fabric_version=0.107.0+1.21.3 # Mod Properties -mod_version = v7.46.2-MC1.21.3 +mod_version = v7.46.3-MC1.21.3 maven_group = net.wurstclient archives_base_name = Wurst-Client diff --git a/src/main/java/net/wurstclient/WurstClient.java b/src/main/java/net/wurstclient/WurstClient.java index c2accd5557..13c375c52c 100644 --- a/src/main/java/net/wurstclient/WurstClient.java +++ b/src/main/java/net/wurstclient/WurstClient.java @@ -50,7 +50,7 @@ public enum WurstClient public static MinecraftClient MC; public static IMinecraftClient IMC; - public static final String VERSION = "7.46.2"; + public static final String VERSION = "7.46.3"; public static final String MC_VERSION = "1.21.3"; private WurstAnalytics analytics; diff --git a/src/main/java/net/wurstclient/commands/CopyItemCmd.java b/src/main/java/net/wurstclient/commands/CopyItemCmd.java index 5c74bb10ce..f30bdeaa48 100644 --- a/src/main/java/net/wurstclient/commands/CopyItemCmd.java +++ b/src/main/java/net/wurstclient/commands/CopyItemCmd.java @@ -9,12 +9,12 @@ import net.minecraft.client.network.AbstractClientPlayerEntity; import net.minecraft.item.ItemStack; -import net.minecraft.network.packet.c2s.play.CreativeInventoryActionC2SPacket; import net.wurstclient.command.CmdError; import net.wurstclient.command.CmdException; import net.wurstclient.command.CmdSyntaxError; import net.wurstclient.command.Command; import net.wurstclient.util.ChatUtils; +import net.wurstclient.util.CmdUtils; public final class CopyItemCmd extends Command { @@ -38,7 +38,7 @@ public void call(String[] args) throws CmdException AbstractClientPlayerEntity player = getPlayer(args[0]); ItemStack item = getItem(player, args[1]); - giveItem(item); + CmdUtils.giveItem(item); ChatUtils.message("Item copied."); } @@ -80,18 +80,4 @@ private ItemStack getItem(AbstractClientPlayerEntity player, String slot) throw new CmdSyntaxError(); } } - - private void giveItem(ItemStack stack) throws CmdError - { - int slot = MC.player.getInventory().getEmptySlot(); - if(slot < 0) - throw new CmdError("Cannot give item. Your inventory is full."); - - if(slot < 9) - slot += 36; - - CreativeInventoryActionC2SPacket packet = - new CreativeInventoryActionC2SPacket(slot, stack); - MC.player.networkHandler.sendPacket(packet); - } } diff --git a/src/main/java/net/wurstclient/commands/GiveCmd.java b/src/main/java/net/wurstclient/commands/GiveCmd.java index 536b76ec26..f7f890fb73 100644 --- a/src/main/java/net/wurstclient/commands/GiveCmd.java +++ b/src/main/java/net/wurstclient/commands/GiveCmd.java @@ -15,18 +15,14 @@ import net.minecraft.component.type.NbtComponent; import net.minecraft.item.Item; import net.minecraft.item.ItemStack; -import net.minecraft.item.Items; import net.minecraft.nbt.NbtCompound; import net.minecraft.nbt.StringNbtReader; -import net.minecraft.network.packet.c2s.play.CreativeInventoryActionC2SPacket; -import net.minecraft.registry.Registries; -import net.minecraft.util.Identifier; -import net.minecraft.util.InvalidIdentifierException; import net.wurstclient.command.CmdError; import net.wurstclient.command.CmdException; import net.wurstclient.command.CmdSyntaxError; import net.wurstclient.command.Command; import net.wurstclient.util.ChatUtils; +import net.wurstclient.util.CmdUtils; import net.wurstclient.util.MathUtils; public final class GiveCmd extends Command @@ -50,13 +46,7 @@ public void call(String[] args) throws CmdException throw new CmdError("Creative mode only."); // id/name - Item item = getItem(args[0]); - - if(item == Items.AIR && MathUtils.isInteger(args[0])) - item = Item.byRawId(Integer.parseInt(args[0])); - - if(item == Items.AIR) - throw new CmdError("Item \"" + args[0] + "\" could not be found."); + Item item = CmdUtils.parseItem(args[0]); // amount int amount = 1; @@ -94,35 +84,7 @@ public void call(String[] args) throws CmdException } // give item - if(!placeStackInHotbar(stack)) - throw new CmdError("Please clear a slot in your hotbar."); + CmdUtils.giveItem(stack); ChatUtils.message("Item" + (amount > 1 ? "s" : "") + " created."); } - - private Item getItem(String id) throws CmdSyntaxError - { - try - { - return Registries.ITEM.get(Identifier.of(id)); - - }catch(InvalidIdentifierException e) - { - throw new CmdSyntaxError("Invalid item: " + id); - } - } - - private boolean placeStackInHotbar(ItemStack stack) - { - for(int i = 0; i < 9; i++) - { - if(!MC.player.getInventory().getStack(i).isEmpty()) - continue; - - MC.player.networkHandler.sendPacket( - new CreativeInventoryActionC2SPacket(36 + i, stack)); - return true; - } - - return false; - } } diff --git a/src/main/java/net/wurstclient/commands/ItemListCmd.java b/src/main/java/net/wurstclient/commands/ItemListCmd.java index 4a9fa08a5a..cc1f138510 100644 --- a/src/main/java/net/wurstclient/commands/ItemListCmd.java +++ b/src/main/java/net/wurstclient/commands/ItemListCmd.java @@ -22,7 +22,6 @@ import net.wurstclient.settings.Setting; import net.wurstclient.util.ChatUtils; import net.wurstclient.util.CmdUtils; -import net.wurstclient.util.ItemUtils; import net.wurstclient.util.MathUtils; @DontBlock @@ -80,11 +79,7 @@ private void add(Feature feature, ItemListSetting setting, String[] args) if(args.length != 4) throw new CmdSyntaxError(); - String inputItemName = args[3]; - Item item = ItemUtils.getItemFromNameOrID(inputItemName); - if(item == null) - throw new CmdSyntaxError( - "\"" + inputItemName + "\" is not a valid item."); + Item item = CmdUtils.parseItem(args[3]); String itemName = Registries.ITEM.getId(item).toString(); int index = Collections.binarySearch(setting.getItemNames(), itemName); @@ -101,11 +96,7 @@ private void remove(Feature feature, ItemListSetting setting, String[] args) if(args.length != 4) throw new CmdSyntaxError(); - String inputItemName = args[3]; - Item item = ItemUtils.getItemFromNameOrID(inputItemName); - if(item == null) - throw new CmdSyntaxError( - "\"" + inputItemName + "\" is not a valid item."); + Item item = CmdUtils.parseItem(args[3]); String itemName = Registries.ITEM.getId(item).toString(); int index = Collections.binarySearch(setting.getItemNames(), itemName); diff --git a/src/main/java/net/wurstclient/serverfinder/ServerFinderScreen.java b/src/main/java/net/wurstclient/serverfinder/ServerFinderScreen.java index 7eed6a1fa3..6d1b4fc159 100644 --- a/src/main/java/net/wurstclient/serverfinder/ServerFinderScreen.java +++ b/src/main/java/net/wurstclient/serverfinder/ServerFinderScreen.java @@ -200,6 +200,7 @@ private void updatePingers(ArrayList pingers) } } pingers.remove(i); + i -= 1; } } diff --git a/src/main/java/net/wurstclient/test/AltManagerTest.java b/src/main/java/net/wurstclient/test/AltManagerTest.java new file mode 100644 index 0000000000..c06cc6f959 --- /dev/null +++ b/src/main/java/net/wurstclient/test/AltManagerTest.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2014-2024 Wurst-Imperium and contributors. + * + * This source code is subject to the terms of the GNU General Public + * License, version 3. If a copy of the GPL was not distributed with this + * file, You can obtain one at: https://www.gnu.org/licenses/gpl-3.0.txt + */ +package net.wurstclient.test; + +import static net.wurstclient.test.WurstClientTestHelper.*; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.screen.TitleScreen; +import net.minecraft.client.gui.widget.ButtonWidget; + +public enum AltManagerTest +{ + ; + + public static void testAltManagerButton(MinecraftClient mc) + { + System.out.println("Checking AltManager button position"); + + if(!(mc.currentScreen instanceof TitleScreen)) + throw new RuntimeException("Not on the title screen"); + + ButtonWidget multiplayerButton = findButton(mc, "menu.multiplayer"); + ButtonWidget realmsButton = findButton(mc, "menu.online"); + ButtonWidget altManagerButton = findButton(mc, "Alt Manager"); + + checkButtonPosition(altManagerButton, realmsButton.getRight() + 4, + multiplayerButton.getBottom() + 4); + } +} diff --git a/src/main/java/net/wurstclient/test/CopyItemCmdTest.java b/src/main/java/net/wurstclient/test/CopyItemCmdTest.java new file mode 100644 index 0000000000..515a819dc6 --- /dev/null +++ b/src/main/java/net/wurstclient/test/CopyItemCmdTest.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2014-2024 Wurst-Imperium and contributors. + * + * This source code is subject to the terms of the GNU General Public + * License, version 3. If a copy of the GPL was not distributed with this + * file, You can obtain one at: https://www.gnu.org/licenses/gpl-3.0.txt + */ +package net.wurstclient.test; + +import static net.wurstclient.test.WurstClientTestHelper.*; + +import net.minecraft.client.option.Perspective; +import net.minecraft.item.Items; + +public enum CopyItemCmdTest +{ + ; + + public static void testCopyItemCmd() + { + System.out.println("Testing .copyitem command"); + setPerspective(Perspective.THIRD_PERSON_FRONT); + + // Put on a golden helmet + runChatCommand("item replace entity @s armor.head with golden_helmet"); + takeScreenshot("copyitem_command_setup"); + assertNoItemInSlot(0); + assertOneItemInSlot(39, Items.GOLDEN_HELMET); + + // .copyitem the helmet + String playerName = submitAndGet(mc -> mc.player.getName().getString()); + runWurstCommand("copyitem " + playerName + " head"); + takeScreenshot("copyitem_command_result"); + assertOneItemInSlot(0, Items.GOLDEN_HELMET); + assertOneItemInSlot(39, Items.GOLDEN_HELMET); + + // Clean up + setPerspective(Perspective.FIRST_PERSON); + runChatCommand("clear"); + clearChat(); + } +} diff --git a/src/main/java/net/wurstclient/test/GiveCmdTest.java b/src/main/java/net/wurstclient/test/GiveCmdTest.java new file mode 100644 index 0000000000..ea9bc5c618 --- /dev/null +++ b/src/main/java/net/wurstclient/test/GiveCmdTest.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2014-2024 Wurst-Imperium and contributors. + * + * This source code is subject to the terms of the GNU General Public + * License, version 3. If a copy of the GPL was not distributed with this + * file, You can obtain one at: https://www.gnu.org/licenses/gpl-3.0.txt + */ +package net.wurstclient.test; + +import static net.wurstclient.test.WurstClientTestHelper.*; + +import net.minecraft.item.Items; + +public enum GiveCmdTest +{ + ; + + public static void testGiveCmd() + { + System.out.println("Testing .give command"); + runWurstCommand("give diamond"); + assertOneItemInSlot(0, Items.DIAMOND); + + // Clean up + runChatCommand("clear"); + clearChat(); + } +} diff --git a/src/main/java/net/wurstclient/test/ModifyCmdTest.java b/src/main/java/net/wurstclient/test/ModifyCmdTest.java new file mode 100644 index 0000000000..8b155aecdf --- /dev/null +++ b/src/main/java/net/wurstclient/test/ModifyCmdTest.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2014-2024 Wurst-Imperium and contributors. + * + * This source code is subject to the terms of the GNU General Public + * License, version 3. If a copy of the GPL was not distributed with this + * file, You can obtain one at: https://www.gnu.org/licenses/gpl-3.0.txt + */ +package net.wurstclient.test; + +import static net.wurstclient.test.WurstClientTestHelper.*; + +import net.minecraft.component.DataComponentTypes; +import net.minecraft.component.type.NbtComponent; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.nbt.NbtCompound; + +public enum ModifyCmdTest +{ + ; + + public static void testModifyCmd() + { + System.out.println("Testing .modify command"); + + // /give a diamond + runChatCommand("give @s diamond"); + assertOneItemInSlot(0, Items.DIAMOND); + + // .modify it with NBT data + runWurstCommand("modify add {test:123}"); + assertOneItemInSlot(0, Items.DIAMOND); + submitAndWait(mc -> { + ItemStack stack = mc.player.getInventory().getMainHandStack(); + NbtCompound nbt = stack.getComponents().getOrDefault( + DataComponentTypes.CUSTOM_DATA, NbtComponent.DEFAULT).copyNbt(); + if(!nbt.contains("test")) + throw new RuntimeException( + "NBT data is missing the 'test' key"); + if(nbt.getInt("test") != 123) + throw new RuntimeException("NBT data is incorrect"); + }); + runWurstCommand("viewnbt"); + takeScreenshot("modify_command_result"); + + // Clean up + runChatCommand("clear"); + clearChat(); + } +} diff --git a/src/main/java/net/wurstclient/test/WurstClientTestHelper.java b/src/main/java/net/wurstclient/test/WurstClientTestHelper.java index e53c967517..548ba797d3 100644 --- a/src/main/java/net/wurstclient/test/WurstClientTestHelper.java +++ b/src/main/java/net/wurstclient/test/WurstClientTestHelper.java @@ -7,43 +7,202 @@ */ package net.wurstclient.test; -import static net.wurstclient.test.fabric.FabricClientTestHelper.*; +import java.io.File; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; import com.mojang.brigadier.ParseResults; import com.mojang.brigadier.exceptions.CommandSyntaxException; +import net.fabricmc.loader.api.FabricLoader; import net.minecraft.client.MinecraftClient; import net.minecraft.client.gui.Drawable; +import net.minecraft.client.gui.screen.GameMenuScreen; import net.minecraft.client.gui.screen.Screen; import net.minecraft.client.gui.screen.TitleScreen; +import net.minecraft.client.gui.screen.ingame.InventoryScreen; +import net.minecraft.client.gui.screen.world.LevelLoadingScreen; import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.gui.widget.ClickableWidget; +import net.minecraft.client.gui.widget.CyclingButtonWidget; import net.minecraft.client.gui.widget.TextFieldWidget; import net.minecraft.client.network.ClientPlayNetworkHandler; +import net.minecraft.client.option.Perspective; import net.minecraft.client.resource.language.I18n; import net.minecraft.client.tutorial.TutorialStep; +import net.minecraft.client.util.ScreenshotRecorder; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.wurstclient.WurstClient; public enum WurstClientTestHelper { ; - public static boolean testAltManagerButton(MinecraftClient mc) + private static final AtomicInteger screenshotCounter = new AtomicInteger(0); + + /** + * Runs the given consumer on Minecraft's main thread and waits for it to + * complete. + */ + public static void submitAndWait(Consumer consumer) { - System.out.println("Checking AltManager button position"); - - if(!(mc.currentScreen instanceof TitleScreen)) - throw new RuntimeException("Not on the title screen"); + MinecraftClient mc = MinecraftClient.getInstance(); + mc.submit(() -> consumer.accept(mc)).join(); + } + + /** + * Runs the given function on Minecraft's main thread, waits for it to + * complete, and returns the result. + */ + public static T submitAndGet(Function function) + { + MinecraftClient mc = MinecraftClient.getInstance(); + return mc.submit(() -> function.apply(mc)).join(); + } + + /** + * Waits for the given duration. + */ + public static void wait(Duration duration) + { + try + { + Thread.sleep(duration.toMillis()); + + }catch(InterruptedException e) + { + throw new RuntimeException(e); + } + } + + /** + * Waits until the given condition is true, or fails if the timeout is + * reached. + */ + public static void waitUntil(String event, + Predicate condition, Duration maxDuration) + { + LocalDateTime startTime = LocalDateTime.now(); + LocalDateTime timeout = startTime.plus(maxDuration); + System.out.println("Waiting until " + event); - ButtonWidget multiplayerButton = findButton(mc, "menu.multiplayer"); - ButtonWidget realmsButton = findButton(mc, "menu.online"); - ButtonWidget altManagerButton = findButton(mc, "Alt Manager"); + while(true) + { + if(submitAndGet(condition::test)) + { + double seconds = + Duration.between(startTime, LocalDateTime.now()).toMillis() + / 1000.0; + System.out.println( + "Waiting until " + event + " took " + seconds + "s"); + break; + } + + if(startTime.isAfter(timeout)) + throw new RuntimeException( + "Waiting until " + event + " took too long"); + + wait(Duration.ofMillis(50)); + } + } + + /** + * Waits until the given condition is true, or fails after 10 seconds. + */ + public static void waitUntil(String event, + Predicate condition) + { + waitUntil(event, condition, Duration.ofSeconds(10)); + } + + /** + * Waits until the given screen is open, or fails after 10 seconds. + */ + public static void waitForScreen(Class screenClass) + { + waitUntil("screen " + screenClass.getName() + " is open", + mc -> screenClass.isInstance(mc.currentScreen)); + } + + /** + * Waits for the fading animation of the title screen to finish, or fails + * after 10 seconds. + */ + public static void waitForTitleScreenFade() + { + waitUntil("title screen fade is complete", mc -> { + if(!(mc.currentScreen instanceof TitleScreen titleScreen)) + return false; + + return !titleScreen.doBackgroundFade; + }); + } + + /** + * Waits until the red overlay with the Mojang logo and progress bar goes + * away, or fails after 5 minutes. + */ + public static void waitForResourceLoading() + { + waitUntil("loading is complete", mc -> mc.getOverlay() == null, + Duration.ofMinutes(5)); + } + + public static void waitForWorldLoad() + { + waitUntil("world is loaded", + mc -> mc.world != null + && !(mc.currentScreen instanceof LevelLoadingScreen), + Duration.ofMinutes(30)); + } + + public static void waitForWorldTicks(int ticks) + { + long startTicks = submitAndGet(mc -> mc.world.getTime()); + waitUntil(ticks + " world ticks have passed", + mc -> mc.world.getTime() >= startTicks + ticks, + Duration.ofMillis(ticks * 100).plusMinutes(5)); + } + + /** + * Waits for 50ms and then takes a screenshot with the given name. + */ + public static void takeScreenshot(String name) + { + takeScreenshot(name, Duration.ofMillis(50)); + } + + /** + * Waits for the given delay and then takes a screenshot with the given + * name. + */ + public static void takeScreenshot(String name, Duration delay) + { + wait(delay); - checkButtonPosition(altManagerButton, realmsButton.getRight() + 4, - multiplayerButton.getBottom() + 4); + String count = + String.format("%02d", screenshotCounter.incrementAndGet()); + String filename = count + "_" + name + ".png"; + File gameDir = FabricLoader.getInstance().getGameDir().toFile(); - return true; + submitAndWait(mc -> ScreenshotRecorder.saveScreenshot(gameDir, filename, + mc.getFramebuffer(), message -> {})); } - private static ButtonWidget findButton(MinecraftClient mc, + /** + * Returns the first button on the current screen that has the given + * translation key, or fails if not found. + * + *

+ * For non-translated buttons, the translationKey parameter should be the + * raw button text instead. + */ + public static ButtonWidget findButton(MinecraftClient mc, String translationKey) { String message = I18n.translate(translationKey); @@ -56,7 +215,11 @@ private static ButtonWidget findButton(MinecraftClient mc, throw new RuntimeException(message + " button could not be found"); } - private static void checkButtonPosition(ButtonWidget button, int expectedX, + /** + * Looks for the given button at the given coordinates and fails if it is + * not there. + */ + public static void checkButtonPosition(ButtonWidget button, int expectedX, int expectedY) { String buttonName = button.getMessage().getString(); @@ -72,34 +235,122 @@ private static void checkButtonPosition(ButtonWidget button, int expectedX, + expectedY + ", actual Y: " + button.getY()); } - public static void setTextfieldText(int index, String text) + /** + * Clicks the button with the given translation key, or fails after 10 + * seconds. + * + *

+ * For non-translated buttons, the translationKey parameter should be the + * raw button text instead. + */ + public static void clickButton(String translationKey) { - waitFor("Set textfield " + index + " to " + text, mc -> { + String buttonText = I18n.translate(translationKey); + + waitUntil("button saying " + buttonText + " is visible", mc -> { Screen screen = mc.currentScreen; if(screen == null) return false; - int currentIndex = 0; + for(Drawable drawable : screen.drawables) + { + if(!(drawable instanceof ClickableWidget widget)) + continue; + + if(widget instanceof ButtonWidget button + && buttonText.equals(button.getMessage().getString())) + { + button.onPress(); + return true; + } + + if(widget instanceof CyclingButtonWidget button + && buttonText.equals(button.optionText.getString())) + { + button.onPress(); + return true; + } + } + + return false; + }); + } + + /** + * Types the given text into the nth text field on the current screen, or + * fails after 10 seconds. + */ + public static void setTextFieldText(int index, String text) + { + waitUntil("text field #" + index + " is visible", mc -> { + Screen screen = mc.currentScreen; + if(screen == null) + return false; + + int i = 0; for(Drawable drawable : screen.drawables) { if(!(drawable instanceof TextFieldWidget textField)) continue; - if(currentIndex == index) + if(i == index) { textField.setText(text); return true; } - currentIndex++; + i++; } return false; }); } + public static void closeScreen() + { + submitAndWait(mc -> mc.setScreen(null)); + } + + public static void openGameMenu() + { + submitAndWait(mc -> mc.setScreen(new GameMenuScreen(true))); + } + + public static void openInventory() + { + submitAndWait(mc -> mc.setScreen(new InventoryScreen(mc.player))); + } + + public static void toggleDebugHud() + { + submitAndWait(mc -> mc.inGameHud.getDebugHud().toggleDebugHud()); + } + + public static void setPerspective(Perspective perspective) + { + submitAndWait(mc -> mc.options.setPerspective(perspective)); + } + + public static void dismissTutorialToasts() + { + submitAndWait(mc -> mc.getTutorialManager().setStep(TutorialStep.NONE)); + } + + public static void clearChat() + { + submitAndWait(mc -> mc.inGameHud.getChatHud().clear(true)); + } + + /** + * Runs the given chat command and waits one tick for the action to + * complete. + * + *

+ * Do not put a / at the start of the command. + */ public static void runChatCommand(String command) { + System.out.println("Running command: /" + command); submitAndWait(mc -> { ClientPlayNetworkHandler netHandler = mc.getNetworkHandler(); @@ -120,23 +371,57 @@ public static void runChatCommand(String command) // Command is valid, send it netHandler.sendChatCommand(command); - return null; }); + waitForWorldTicks(1); } - public static void clearChat() + /** + * Runs the given Wurst command. + * + *

+ * Do not put a . at the start of the command. + */ + public static void runWurstCommand(String command) { + System.out.println("Running command: ." + command); submitAndWait(mc -> { - mc.inGameHud.getChatHud().clear(true); - return null; + WurstClient.INSTANCE.getCmdProcessor().process(command); }); } - public static void dismissTutorialToasts() + /** + * Uses the currently held item and/or targeted block/entity, then waits + * one tick for the action to complete. + * + *

+ * This won't work for right clicking in menus. + */ + public static void rightClickInGame() + { + submitAndWait(mc -> mc.doItemUse()); + waitForWorldTicks(1); + } + + public static void assertOneItemInSlot(int slot, Item item) + { + submitAndWait(mc -> { + ItemStack stack = mc.player.getInventory().getStack(slot); + if(!stack.isOf(item) || stack.getCount() != 1) + throw new RuntimeException( + "Expected 1 " + item.getName().getString() + " at slot " + + slot + ", found " + stack.getCount() + " " + + stack.getItem().getName().getString() + " instead"); + }); + } + + public static void assertNoItemInSlot(int slot) { submitAndWait(mc -> { - mc.getTutorialManager().setStep(TutorialStep.NONE); - return null; + ItemStack stack = mc.player.getInventory().getStack(slot); + if(!stack.isEmpty()) + throw new RuntimeException("Expected no item in slot " + slot + + ", found " + stack.getCount() + " " + + stack.getItem().getName().getString() + " instead"); }); } } diff --git a/src/main/java/net/wurstclient/test/WurstE2ETestClient.java b/src/main/java/net/wurstclient/test/WurstE2ETestClient.java index d099cfa56b..480a13e740 100644 --- a/src/main/java/net/wurstclient/test/WurstE2ETestClient.java +++ b/src/main/java/net/wurstclient/test/WurstE2ETestClient.java @@ -8,7 +8,6 @@ package net.wurstclient.test; import static net.wurstclient.test.WurstClientTestHelper.*; -import static net.wurstclient.test.fabric.FabricClientTestHelper.*; import java.time.Duration; @@ -21,8 +20,6 @@ import net.minecraft.client.gui.screen.world.CreateWorldScreen; import net.minecraft.client.gui.screen.world.SelectWorldScreen; -// It would be cleaner to have this test in src/test/java, but remapping that -// into a separate testmod is a whole can of worms. public final class WurstE2ETestClient implements ModInitializer { @Override @@ -35,20 +32,20 @@ public void onInitialize() .uncaughtExceptionHandler((t, e) -> { e.printStackTrace(); System.exit(1); - }).start(this::runTest); + }).start(this::runTests); } - private void runTest() + private void runTests() { - System.out.println("Starting Wurst End-to-End Test"); - waitForLoadingComplete(); + System.out.println("Starting Wurst End-to-End Tests"); + waitForResourceLoading(); - if(submitAndWait(mc -> mc.options.onboardAccessibility)) + if(submitAndGet(mc -> mc.options.onboardAccessibility)) { System.out.println("Onboarding is enabled. Waiting for it"); waitForScreen(AccessibilityOnboardingScreen.class); System.out.println("Reached onboarding screen"); - clickScreenButton("gui.continue"); + clickButton("gui.continue"); } waitForScreen(TitleScreen.class); @@ -56,54 +53,53 @@ private void runTest() System.out.println("Reached title screen"); takeScreenshot("title_screen", Duration.ZERO); - submitAndWait(WurstClientTestHelper::testAltManagerButton); + submitAndWait(AltManagerTest::testAltManagerButton); // TODO: Test more of AltManager System.out.println("Clicking singleplayer button"); - clickScreenButton("menu.singleplayer"); + clickButton("menu.singleplayer"); - if(submitAndWait(mc -> !mc.getLevelStorage().getLevelList().isEmpty())) + if(submitAndGet(mc -> !mc.getLevelStorage().getLevelList().isEmpty())) { System.out.println("World list is not empty. Waiting for it"); waitForScreen(SelectWorldScreen.class); System.out.println("Reached select world screen"); takeScreenshot("select_world_screen"); - clickScreenButton("selectWorld.create"); + clickButton("selectWorld.create"); } waitForScreen(CreateWorldScreen.class); System.out.println("Reached create world screen"); // Set MC version as world name - setTextfieldText(0, SharedConstants.getGameVersion().getName()); + setTextFieldText(0, + "E2E Test " + SharedConstants.getGameVersion().getName()); // Select creative mode - clickScreenButton("selectWorld.gameMode"); - clickScreenButton("selectWorld.gameMode"); + clickButton("selectWorld.gameMode"); + clickButton("selectWorld.gameMode"); takeScreenshot("create_world_screen"); System.out.println("Creating test world"); - clickScreenButton("selectWorld.create"); + clickButton("selectWorld.create"); - waitForWorldTicks(180); + waitForWorldLoad(); dismissTutorialToasts(); + waitForWorldTicks(200); runChatCommand("seed"); - waitForWorldTicks(20); System.out.println("Reached singleplayer world"); takeScreenshot("in_game", Duration.ZERO); clearChat(); System.out.println("Opening debug menu"); - enableDebugHud(); + toggleDebugHud(); takeScreenshot("debug_menu"); System.out.println("Closing debug menu"); - enableDebugHud();// bad name, it actually toggles + toggleDebugHud(); System.out.println("Checking for broken mixins"); MixinEnvironment.getCurrentEnvironment().audit(); - // TODO: Test some Wurst hacks - System.out.println("Opening inventory"); openInventory(); takeScreenshot("inventory"); @@ -113,6 +109,15 @@ private void runTest() // TODO: Open ClickGUI and Navigator + // Clear inventory and chat before running tests + runChatCommand("clear"); + clearChat(); + + CopyItemCmdTest.testCopyItemCmd(); + GiveCmdTest.testGiveCmd(); + ModifyCmdTest.testModifyCmd(); + // TODO: Test more Wurst hacks + System.out.println("Opening game menu"); openGameMenu(); takeScreenshot("game_menu"); @@ -120,10 +125,10 @@ private void runTest() // TODO: Check Wurst Options System.out.println("Returning to title screen"); - clickScreenButton("menu.returnToMenu"); + clickButton("menu.returnToMenu"); waitForScreen(TitleScreen.class); System.out.println("Stopping the game"); - clickScreenButton("menu.quit"); + clickButton("menu.quit"); } } diff --git a/src/main/java/net/wurstclient/test/fabric/FabricClientTestHelper.java b/src/main/java/net/wurstclient/test/fabric/FabricClientTestHelper.java deleted file mode 100644 index c4309b2485..0000000000 --- a/src/main/java/net/wurstclient/test/fabric/FabricClientTestHelper.java +++ /dev/null @@ -1,264 +0,0 @@ -/* - * Copied from https://github.com/FabricMC/fabric/blob/ - * f17fc976e9d0d6fe6fc7303fb25ca7b24d122c98/fabric-api-base/src/testmodClient/ - * java/net/fabricmc/fabric/test/base/client/FabricClientTestHelper.java - * and formatted to match our code style. - * - * This isn't a part of Fabric's public API (yet), so for now the only way to - * use it is to make a copy. - */ -/* - * 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.wurstclient.test.fabric; - -import java.time.Duration; -import java.time.LocalDateTime; -import java.util.Objects; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Function; -import java.util.function.Predicate; - -import net.fabricmc.loader.api.FabricLoader; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.gui.Drawable; -import net.minecraft.client.gui.screen.GameMenuScreen; -import net.minecraft.client.gui.screen.Screen; -import net.minecraft.client.gui.screen.TitleScreen; -import net.minecraft.client.gui.screen.ingame.CreativeInventoryScreen; -import net.minecraft.client.gui.screen.ingame.InventoryScreen; -import net.minecraft.client.gui.screen.world.LevelLoadingScreen; -import net.minecraft.client.gui.widget.ButtonWidget; -import net.minecraft.client.gui.widget.ClickableWidget; -import net.minecraft.client.gui.widget.CyclingButtonWidget; -import net.minecraft.client.gui.widget.PressableWidget; -import net.minecraft.client.gui.widget.Widget; -import net.minecraft.client.option.Perspective; -import net.minecraft.client.util.ScreenshotRecorder; -import net.minecraft.text.Text; - -// Provides thread safe utils for interacting with a running game. -public final class FabricClientTestHelper -{ - // Added a counter so that screenshots show up in the correct order - private static final AtomicInteger screenshotCount = new AtomicInteger(0); - - public static void waitForLoadingComplete() - { - waitFor("Loading to complete", client -> client.getOverlay() == null, - Duration.ofMinutes(5)); - } - - public static void waitForScreen(Class screenClass) - { - waitFor("Screen %s".formatted(screenClass.getName()), - client -> client.currentScreen != null - && client.currentScreen.getClass() == screenClass); - } - - public static void openGameMenu() - { - setScreen(client -> new GameMenuScreen(true)); - waitForScreen(GameMenuScreen.class); - } - - public static void openInventory() - { - setScreen(client -> new InventoryScreen( - Objects.requireNonNull(client.player))); - - boolean creative = submitAndWait( - client -> Objects.requireNonNull(client.player).isCreative()); - waitForScreen( - creative ? CreativeInventoryScreen.class : InventoryScreen.class); - } - - public static void closeScreen() - { - setScreen(client -> null); - } - - private static void setScreen( - Function screenSupplier) - { - submit(client -> { - client.setScreen(screenSupplier.apply(client)); - return null; - }); - } - - public static void takeScreenshot(String name) - { - takeScreenshot(name, Duration.ofMillis(50)); - } - - public static void takeScreenshot(String name, Duration delay) - { - // Allow time for any screens to open - waitFor(delay); - - submitAndWait(client -> { - // Added a counter so that screenshots show up in the correct order - String count = - String.format("%02d", screenshotCount.incrementAndGet()); - String filename = count + "_" + name + ".png"; - ScreenshotRecorder.saveScreenshot( - FabricLoader.getInstance().getGameDir().toFile(), filename, - client.getFramebuffer(), message -> {}); - return null; - }); - } - - public static void clickScreenButton(String translationKey) - { - final String buttonText = Text.translatable(translationKey).getString(); - - waitFor("Click button" + buttonText, client -> { - final Screen screen = client.currentScreen; - - if(screen == null) - return false; - - // Replaced the accessor with an access widener - for(Drawable drawable : screen.drawables) - { - if(drawable instanceof PressableWidget pressableWidget - && pressMatchingButton(pressableWidget, buttonText)) - return true; - - if(drawable instanceof Widget widget) - widget.forEachChild(clickableWidget -> pressMatchingButton( - clickableWidget, buttonText)); - } - - // Was unable to find the button to press - return false; - }); - } - - private static boolean pressMatchingButton(ClickableWidget widget, - String text) - { - if(widget instanceof ButtonWidget buttonWidget - && text.equals(buttonWidget.getMessage().getString())) - { - buttonWidget.onPress(); - return true; - } - - // Replaced the accessor with an access widener - if(widget instanceof CyclingButtonWidget buttonWidget - && text.equals(buttonWidget.optionText.getString())) - { - buttonWidget.onPress(); - return true; - } - - return false; - } - - public static void waitForWorldTicks(long ticks) - { - // Wait for the world to be loaded and get the start ticks - waitFor("World load", - client -> client.world != null - && !(client.currentScreen instanceof LevelLoadingScreen), - Duration.ofMinutes(30)); - final long startTicks = submitAndWait(client -> client.world.getTime()); - waitFor("World load", client -> Objects.requireNonNull(client.world) - .getTime() > startTicks + ticks, Duration.ofMinutes(10)); - } - - public static void enableDebugHud() - { - submitAndWait(client -> { - client.inGameHud.getDebugHud().toggleDebugHud(); - return null; - }); - } - - public static void setPerspective(Perspective perspective) - { - submitAndWait(client -> { - client.options.setPerspective(perspective); - return null; - }); - } - - // Removed connectToServer() - - public static void waitForTitleScreenFade() - { - waitFor("Title screen fade", client -> { - if(!(client.currentScreen instanceof TitleScreen titleScreen)) - return false; - - // Replaced the accessor with an access widener - return !titleScreen.doBackgroundFade; - }); - } - - public static void waitFor(String what, - Predicate predicate) - { - waitFor(what, predicate, Duration.ofSeconds(10)); - } - - public static void waitFor(String what, - Predicate predicate, Duration timeout) - { - final LocalDateTime end = LocalDateTime.now().plus(timeout); - - while(true) - { - boolean result = submitAndWait(predicate::test); - - if(result) - break; - - if(LocalDateTime.now().isAfter(end)) - throw new RuntimeException("Timed out waiting for " + what); - - waitFor(Duration.ofMillis(50)); - } - } - - private static void waitFor(Duration duration) - { - try - { - Thread.sleep(duration.toMillis()); - - }catch(InterruptedException e) - { - throw new RuntimeException(e); - } - } - - @SuppressWarnings("resource") - private static CompletableFuture submit( - Function function) - { - return MinecraftClient.getInstance() - .submit(() -> function.apply(MinecraftClient.getInstance())); - } - - public static T submitAndWait(Function function) - { - return submit(function).join(); - } -} diff --git a/src/main/java/net/wurstclient/util/CmdUtils.java b/src/main/java/net/wurstclient/util/CmdUtils.java index 816d58a19d..00ac9305f8 100644 --- a/src/main/java/net/wurstclient/util/CmdUtils.java +++ b/src/main/java/net/wurstclient/util/CmdUtils.java @@ -9,15 +9,23 @@ import java.util.stream.Stream; +import net.minecraft.client.MinecraftClient; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.network.packet.c2s.play.CreativeInventoryActionC2SPacket; import net.wurstclient.Feature; import net.wurstclient.WurstClient; import net.wurstclient.command.CmdError; +import net.wurstclient.command.CmdSyntaxError; import net.wurstclient.settings.Setting; public enum CmdUtils { ; + private static final MinecraftClient MC = WurstClient.MC; + public static Feature findFeature(String name) throws CmdError { Stream stream = @@ -44,4 +52,29 @@ public static Setting findSetting(Feature feature, String name) return setting; } + + public static Item parseItem(String nameOrId) throws CmdSyntaxError + { + Item item = ItemUtils.getItemFromNameOrID(nameOrId); + + if(item == null) + throw new CmdSyntaxError( + "\"" + nameOrId + "\" is not a valid item."); + + return item; + } + + public static void giveItem(ItemStack stack) throws CmdError + { + PlayerInventory inventory = MC.player.getInventory(); + int slot = inventory.getEmptySlot(); + if(slot < 0) + throw new CmdError("Cannot give item. Your inventory is full."); + + inventory.setStack(slot, stack); + CreativeInventoryActionC2SPacket packet = + new CreativeInventoryActionC2SPacket( + InventoryUtils.toNetworkSlot(slot), stack); + MC.player.networkHandler.sendPacket(packet); + } } diff --git a/src/main/java/net/wurstclient/util/InventoryUtils.java b/src/main/java/net/wurstclient/util/InventoryUtils.java index a067f0a371..eaf18cfa9e 100644 --- a/src/main/java/net/wurstclient/util/InventoryUtils.java +++ b/src/main/java/net/wurstclient/util/InventoryUtils.java @@ -101,6 +101,9 @@ public static int count(Predicate predicate, int maxInvSlot) * Counts the number of items in the player's inventory that match the given * predicate, searching from slot 0 to {@code maxInvSlot-1}. * + *

+ * Note: Attempting to count empty slots will always return 0. + * * @param predicate * checks if an item should be counted * @param maxInvSlot @@ -114,8 +117,10 @@ public static int count(Predicate predicate, int maxInvSlot) public static int count(Predicate predicate, int maxInvSlot, boolean includeOffhand) { - return (int)getMatchingSlots(predicate, maxInvSlot, includeOffhand) - .count(); + PlayerInventory inventory = MC.player.getInventory(); + + return getMatchingSlots(predicate, maxInvSlot, includeOffhand) + .map(slot -> inventory.getStack(slot).getCount()).sum(); } private static IntStream getMatchingSlots(Predicate predicate, diff --git a/src/main/java/net/wurstclient/util/ItemUtils.java b/src/main/java/net/wurstclient/util/ItemUtils.java index 4cc9a9a978..11c82a6b26 100644 --- a/src/main/java/net/wurstclient/util/ItemUtils.java +++ b/src/main/java/net/wurstclient/util/ItemUtils.java @@ -43,8 +43,8 @@ public static Item getItemFromNameOrID(String nameOrId) { if(MathUtils.isInteger(nameOrId)) { - // There is no getOrEmpty() for raw IDs, so this detects when the - // Registry defaults and returns null instead + // There is no getOptionalValue() for raw IDs, so this detects when + // the registry defaults and returns null instead int id = Integer.parseInt(nameOrId); Item item = Registries.ITEM.get(id); if(id != 0 && Registries.ITEM.getRawId(item) == 0) @@ -55,6 +55,8 @@ public static Item getItemFromNameOrID(String nameOrId) try { + // getOptionalValue() returns null instead of Items.AIR if the + // requested item doesn't exist return Registries.ITEM.getOptionalValue(Identifier.of(nameOrId)) .orElse(null);