diff --git a/README.md b/README.md index d7cb4435..85cbcb84 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,8 @@ Folgende Funktionen sind nur für Spieler ohne Premium-Rang verfügbar, da Spiel - Für den Pizzalieferanten-Job wird `/getpizza` automatisch ausgeführt, bis 10 Pizzen gesammelt wurden - Es werden Countdowns angezeigt, bis ein Job wieder ausgeführt werden kann - Aktive Mining XP-Booster werden angezeigt +- Das Angel-Captcha wird zusätzlich in der Mitte des Bildschirms angezeigt, damit es nicht vom Chat verdeckt wird +- Nach der erfolgreichen Eingabe des Angel-Captchas wird die Angel automatisch wieder ausgeworfen ### Widgets diff --git a/src/main/java/de/rettichlp/ucutils/common/Storage.java b/src/main/java/de/rettichlp/ucutils/common/Storage.java index 492152b5..5d83faca 100644 --- a/src/main/java/de/rettichlp/ucutils/common/Storage.java +++ b/src/main/java/de/rettichlp/ucutils/common/Storage.java @@ -11,6 +11,7 @@ import lombok.AllArgsConstructor; import lombok.Getter; import lombok.Setter; +import net.minecraft.component.type.MapIdComponent; import net.minecraft.entity.vehicle.MinecartEntity; import net.minecraft.text.Text; import net.minecraft.util.math.Vec3d; @@ -64,6 +65,11 @@ public class Storage { @Nullable private Vec3d blackMarketPosition; + @Getter + @Setter + @Nullable + private MapIdComponent captchaMap; + @Getter @Setter private boolean carLocked = true; @@ -119,6 +125,8 @@ public void print() { LOGGER.info("activeShutdowns[{}]: {}", this.activeShutdowns.size(), this.activeShutdowns); // blackMarketPosition LOGGER.info("blackMarketPosition: {}", this.blackMarketPosition); + // captchaMapImage + LOGGER.info("captchaMap: {}", this.captchaMap); // countdowns LOGGER.info("countdowns[{}]: {}", this.countdowns.size(), this.countdowns); // dealerPosition diff --git a/src/main/java/de/rettichlp/ucutils/listener/impl/job/AnglerListener.java b/src/main/java/de/rettichlp/ucutils/listener/impl/job/AnglerListener.java new file mode 100644 index 00000000..a8a2b70a --- /dev/null +++ b/src/main/java/de/rettichlp/ucutils/listener/impl/job/AnglerListener.java @@ -0,0 +1,108 @@ +package de.rettichlp.ucutils.listener.impl.job; + +import de.rettichlp.ucutils.common.registry.UCUtilsListener; +import de.rettichlp.ucutils.listener.IMessageReceiveListener; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ClientPlayerInteractionManager; +import net.minecraft.component.type.MapIdComponent; +import net.minecraft.item.ItemStack; +import net.minecraft.item.map.MapState; +import net.minecraft.text.Text; +import org.jetbrains.annotations.Nullable; + +import java.awt.image.BufferedImage; +import java.io.File; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static de.rettichlp.ucutils.UCUtils.LOGGER; +import static de.rettichlp.ucutils.UCUtils.player; +import static de.rettichlp.ucutils.UCUtils.storage; +import static java.awt.image.BufferedImage.TYPE_INT_RGB; +import static java.util.regex.Pattern.compile; +import static javax.imageio.ImageIO.write; +import static net.minecraft.block.MapColor.getRenderColor; +import static net.minecraft.component.DataComponentTypes.MAP_ID; +import static net.minecraft.item.FilledMapItem.getMapState; +import static net.minecraft.item.Items.FILLED_MAP; +import static net.minecraft.item.Items.FISHING_ROD; +import static net.minecraft.util.Hand.MAIN_HAND; + +@UCUtilsListener +public class AnglerListener implements IMessageReceiveListener { + + private static final Pattern ANGLER_CAPTCHA_PATTERN = compile("^\\[AntiBot] Zufällige Überprüfung$"); + private static final Pattern ANGLER_CAPTCHA_CONTINUED_PATTERN = compile("^Deine Combo wurde fortgesetzt! \\(\\d+\\)$"); + + @Override + public boolean onMessageReceive(Text text, String message) { + MinecraftClient client = MinecraftClient.getInstance(); + + Matcher anglerCaptchaMatcher = ANGLER_CAPTCHA_PATTERN.matcher(message); + if (anglerCaptchaMatcher.find()) { + ItemStack offHandStack = player.getOffHandStack(); + if (!offHandStack.isOf(FILLED_MAP)) { + return true; + } + + MapIdComponent captchaMap = offHandStack.get(MAP_ID); + storage.setCaptchaMap(captchaMap); + + saveCaptchaImage(captchaMap); + + return true; + } + + Matcher anglerCaptchaContinuedMatcher = ANGLER_CAPTCHA_CONTINUED_PATTERN.matcher(message); + if (anglerCaptchaContinuedMatcher.find()) { + storage.setCaptchaMap(null); + + ClientPlayerInteractionManager interactionManager = client.interactionManager; + if (interactionManager == null || !player.getMainHandStack().isOf(FISHING_ROD)) { + return true; + } + + interactionManager.interactItem(player, MAIN_HAND); + player.swingHand(MAIN_HAND); + return true; + } + + return true; + } + + private void saveCaptchaImage(MapIdComponent captchaMap) { + BufferedImage mapImage = getMapImage(captchaMap); + if (mapImage == null) { + return; + } + + File outputFile = new File("screenshots/ucutils/captcha.png"); + + try { + outputFile.mkdirs(); + outputFile.createNewFile(); + write(mapImage, "PNG", outputFile); + } catch (Exception e) { + LOGGER.error("Failed to save captcha image", e); + } + } + + private @Nullable BufferedImage getMapImage(MapIdComponent mapIdComponent) { + MapState mapState = getMapState(mapIdComponent, MinecraftClient.getInstance().world); + if (mapState == null) { + return null; + } + + BufferedImage mapImage = new BufferedImage(128, 128, TYPE_INT_RGB); + + for (int i = 0; i < mapState.colors.length; i++) { + int x = i % 128; + int y = i / 128; + + int argb = getRenderColor(mapState.colors[i]); + mapImage.setRGB(x, y, argb); + } + + return mapImage; + } +} diff --git a/src/main/java/de/rettichlp/ucutils/listener/impl/job/DeepSeaFisherListener.java b/src/main/java/de/rettichlp/ucutils/listener/impl/job/DeepSeaFisherListener.java new file mode 100644 index 00000000..c520b8e8 --- /dev/null +++ b/src/main/java/de/rettichlp/ucutils/listener/impl/job/DeepSeaFisherListener.java @@ -0,0 +1,138 @@ +package de.rettichlp.ucutils.listener.impl.job; + +import de.rettichlp.ucutils.common.registry.UCUtilsListener; +import de.rettichlp.ucutils.listener.IMessageReceiveListener; +import de.rettichlp.ucutils.listener.INaviSpotReachedListener; +import lombok.AllArgsConstructor; +import lombok.Getter; +import net.minecraft.text.Text; +import net.minecraft.util.math.BlockPos; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Unmodifiable; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static de.rettichlp.ucutils.UCUtils.commandService; +import static de.rettichlp.ucutils.UCUtils.player; +import static java.lang.Double.compare; +import static java.util.Arrays.stream; +import static java.util.regex.Pattern.compile; + +@UCUtilsListener +public class DeepSeaFisherListener implements IMessageReceiveListener, INaviSpotReachedListener { + + private static final Pattern DEEP_SEA_FISHER_START_PATTERN = compile("^\\[Fischer] Mit /findschwarm kannst du dir den nächsten Fischschwarm anzeigen lassen\\.$"); + private static final Pattern DEEP_SEA_FISHER_SPOT_FOUND_PATTERN = compile("^\\[Fischer] Du hast einen Fischschwarm gefunden!$"); + private static final Pattern DEEP_SEA_FISHER_SPOT_LOST_PATTERN = compile("^\\[Fischer] Du hast dich dem Fischschwarm zu weit entfernt\\.$"); + private static final Pattern DEEP_SEA_FISHER_CATCH_SUCCESS_PATTERN = compile("^\\[Fischer] Du hast \\d+kg frischen Fisch gefangen! \\(\\d+kg\\)$"); + private static final Pattern DEEP_SEA_FISHER_CATCH_FAILURE_PATTERN = compile("^\\[Fischer] Du hast das Fischernetz verloren\\.\\.\\.$"); + private static final int NET_AMOUNT = 5; + + private Collection visitedDeepSeaFisherJobSpots = new ArrayList<>(); + private boolean onDeepSeaFisherSpot = false; + private boolean canCatchFish = true; + + @Override + public boolean onMessageReceive(Text text, String message) { + Matcher deepSeaFisherStartMatcher = DEEP_SEA_FISHER_START_PATTERN.matcher(message); + if (deepSeaFisherStartMatcher.find()) { + this.visitedDeepSeaFisherJobSpots = new ArrayList<>(); + DeepSeaFisherJobSpot.SPOT_1.startNavigation(); + return true; + } + + Matcher deepSeaFisherSpotFoundMatcher = DEEP_SEA_FISHER_SPOT_FOUND_PATTERN.matcher(message); + if (deepSeaFisherSpotFoundMatcher.find()) { + commandService.sendCommand("stoproute"); + this.onDeepSeaFisherSpot = true; + + if (this.canCatchFish) { + startFishing(); + } + + return true; + } + + Matcher deepSeaFisherSpotLostMatcher = DEEP_SEA_FISHER_SPOT_LOST_PATTERN.matcher(message); + if (deepSeaFisherSpotLostMatcher.find()) { + this.onDeepSeaFisherSpot = false; + return true; + } + + Matcher deepSeaFisherCatchSuccessMatcher = DEEP_SEA_FISHER_CATCH_SUCCESS_PATTERN.matcher(message); + Matcher deepSeaFisherCatchFailureMatcher = DEEP_SEA_FISHER_CATCH_FAILURE_PATTERN.matcher(message); + if (deepSeaFisherCatchSuccessMatcher.find() || deepSeaFisherCatchFailureMatcher.find()) { + this.canCatchFish = true; + + // if already on the next fishing spot, start fishing again + if (this.onDeepSeaFisherSpot) { + startFishing(); + } + } + + return true; + } + + @Override + public void onNaviSpotReached() { + if (this.visitedDeepSeaFisherJobSpots.size() == NET_AMOUNT) { + this.visitedDeepSeaFisherJobSpots = new ArrayList<>(); + commandService.sendCommand("dropfish"); + } + } + + private void startFishing() { + this.onDeepSeaFisherSpot = false; + this.canCatchFish = false; + commandService.sendCommand("catchfish"); + + // add the current spot to visited spots + getNearestFisherJobSpot(getNotVisitedFisherJobSpots()).ifPresent(this.visitedDeepSeaFisherJobSpots::add); + + if (this.visitedDeepSeaFisherJobSpots.size() == NET_AMOUNT) { + return; + } + + // get nearest next spot and start navigation + Optional nearestNextFisherJobSpot = getNearestFisherJobSpot(getNotVisitedFisherJobSpots()); + nearestNextFisherJobSpot.ifPresent(DeepSeaFisherJobSpot::startNavigation); + } + + private @NotNull Optional getNearestFisherJobSpot(@NotNull Collection deepSeaFisherJobSpots) { + return deepSeaFisherJobSpots.stream() + .min((spot1, spot2) -> { + double distance1 = player.squaredDistanceTo(spot1.getPosition().getX(), spot1.getPosition().getY(), spot1.getPosition().getZ()); + double distance2 = player.squaredDistanceTo(spot2.getPosition().getX(), spot2.getPosition().getY(), spot2.getPosition().getZ()); + return compare(distance1, distance2); + }); + } + + private @NotNull @Unmodifiable List getNotVisitedFisherJobSpots() { + return stream(DeepSeaFisherJobSpot.values()) + .filter(deepSeaFisherJobSpot -> !this.visitedDeepSeaFisherJobSpots.contains(deepSeaFisherJobSpot)) + .toList(); + } + + @Getter + @AllArgsConstructor + private enum DeepSeaFisherJobSpot { + + SPOT_1(new BlockPos(-571, 63, 160)), + SPOT_2(new BlockPos(-554, 63, 107)), + SPOT_3(new BlockPos(-568, 63, 50)), + SPOT_4(new BlockPos(-522, 63, 10)), + SPOT_5(new BlockPos(-521, 63, 78)); + + private final BlockPos position; + + public void startNavigation() { + String naviCommandString = "navi " + this.position.getX() + "/" + this.position.getY() + "/" + this.position.getZ(); + commandService.sendCommand(naviCommandString); + } + } +} diff --git a/src/main/java/de/rettichlp/ucutils/listener/impl/job/FisherListener.java b/src/main/java/de/rettichlp/ucutils/listener/impl/job/FisherListener.java deleted file mode 100644 index e4c211bc..00000000 --- a/src/main/java/de/rettichlp/ucutils/listener/impl/job/FisherListener.java +++ /dev/null @@ -1,138 +0,0 @@ -package de.rettichlp.ucutils.listener.impl.job; - -import de.rettichlp.ucutils.common.registry.UCUtilsListener; -import de.rettichlp.ucutils.listener.IMessageReceiveListener; -import de.rettichlp.ucutils.listener.INaviSpotReachedListener; -import lombok.AllArgsConstructor; -import lombok.Getter; -import net.minecraft.text.Text; -import net.minecraft.util.math.BlockPos; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Unmodifiable; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Optional; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import static de.rettichlp.ucutils.UCUtils.commandService; -import static de.rettichlp.ucutils.UCUtils.player; -import static java.lang.Double.compare; -import static java.util.Arrays.stream; -import static java.util.regex.Pattern.compile; - -@UCUtilsListener -public class FisherListener implements IMessageReceiveListener, INaviSpotReachedListener { - - private static final Pattern FISHER_START = compile("^\\[Fischer] Mit /findschwarm kannst du dir den nächsten Fischschwarm anzeigen lassen\\.$"); - private static final Pattern FISHER_SPOT_FOUND_PATTERN = compile("^\\[Fischer] Du hast einen Fischschwarm gefunden!$"); - private static final Pattern FISHER_SPOT_LOST_PATTERN = compile("^\\[Fischer] Du hast dich dem Fischschwarm zu weit entfernt\\.$"); - private static final Pattern FISHER_CATCH_SUCCESS = compile("^\\[Fischer] Du hast \\d+kg frischen Fisch gefangen! \\(\\d+kg\\)$"); - private static final Pattern FISHER_CATCH_FAILURE = compile("^\\[Fischer] Du hast das Fischernetz verloren\\.\\.\\.$"); - private static final int NET_AMOUNT = 5; - - private Collection visitedFisherJobSpots = new ArrayList<>(); - private boolean onFisherSpot = false; - private boolean canCatchFish = true; - - @Override - public boolean onMessageReceive(Text text, String message) { - Matcher fisherStartMatcher = FISHER_START.matcher(message); - if (fisherStartMatcher.find()) { - this.visitedFisherJobSpots = new ArrayList<>(); - FisherJobSpot.SPOT_1.startNavigation(); - return true; - } - - Matcher fisherSpotFoundMatcher = FISHER_SPOT_FOUND_PATTERN.matcher(message); - if (fisherSpotFoundMatcher.find()) { - commandService.sendCommand("stoproute"); - this.onFisherSpot = true; - - if (this.canCatchFish) { - startFishing(); - } - - return true; - } - - Matcher fisherSpotLostMatcher = FISHER_SPOT_LOST_PATTERN.matcher(message); - if (fisherSpotLostMatcher.find()) { - this.onFisherSpot = false; - return true; - } - - Matcher fisherCatchSuccessMatcher = FISHER_CATCH_SUCCESS.matcher(message); - Matcher fisherCatchFailureMatcher = FISHER_CATCH_FAILURE.matcher(message); - if (fisherCatchSuccessMatcher.find() || fisherCatchFailureMatcher.find()) { - this.canCatchFish = true; - - // if already on the next fishing spot, start fishing again - if (this.onFisherSpot) { - startFishing(); - } - } - - return true; - } - - @Override - public void onNaviSpotReached() { - if (this.visitedFisherJobSpots.size() == NET_AMOUNT) { - this.visitedFisherJobSpots = new ArrayList<>(); - commandService.sendCommand("dropfish"); - } - } - - private void startFishing() { - this.onFisherSpot = false; - this.canCatchFish = false; - commandService.sendCommand("catchfish"); - - // add the current spot to visited spots - getNearestFisherJobSpot(getNotVisitedFisherJobSpots()).ifPresent(this.visitedFisherJobSpots::add); - - if (this.visitedFisherJobSpots.size() == NET_AMOUNT) { - return; - } - - // get nearest next spot and start navigation - Optional nearestNextFisherJobSpot = getNearestFisherJobSpot(getNotVisitedFisherJobSpots()); - nearestNextFisherJobSpot.ifPresent(FisherJobSpot::startNavigation); - } - - private @NotNull Optional getNearestFisherJobSpot(@NotNull Collection fisherJobSpots) { - return fisherJobSpots.stream() - .min((spot1, spot2) -> { - double distance1 = player.squaredDistanceTo(spot1.getPosition().getX(), spot1.getPosition().getY(), spot1.getPosition().getZ()); - double distance2 = player.squaredDistanceTo(spot2.getPosition().getX(), spot2.getPosition().getY(), spot2.getPosition().getZ()); - return compare(distance1, distance2); - }); - } - - private @NotNull @Unmodifiable List getNotVisitedFisherJobSpots() { - return stream(FisherJobSpot.values()) - .filter(fisherJobSpot -> !this.visitedFisherJobSpots.contains(fisherJobSpot)) - .toList(); - } - - @Getter - @AllArgsConstructor - private enum FisherJobSpot { - - SPOT_1(new BlockPos(-571, 63, 160)), - SPOT_2(new BlockPos(-554, 63, 107)), - SPOT_3(new BlockPos(-568, 63, 50)), - SPOT_4(new BlockPos(-522, 63, 10)), - SPOT_5(new BlockPos(-521, 63, 78)); - - private final BlockPos position; - - public void startNavigation() { - String naviCommandString = "navi " + this.position.getX() + "/" + this.position.getY() + "/" + this.position.getZ(); - commandService.sendCommand(naviCommandString); - } - } -} diff --git a/src/main/java/de/rettichlp/ucutils/mixin/InGameHudMixin.java b/src/main/java/de/rettichlp/ucutils/mixin/InGameHudMixin.java index e9af07e5..50c27a29 100644 --- a/src/main/java/de/rettichlp/ucutils/mixin/InGameHudMixin.java +++ b/src/main/java/de/rettichlp/ucutils/mixin/InGameHudMixin.java @@ -1,13 +1,17 @@ package de.rettichlp.ucutils.mixin; import com.llamalad7.mixinextras.sugar.Local; +import net.minecraft.client.MinecraftClient; import net.minecraft.client.font.TextRenderer; import net.minecraft.client.gui.DrawContext; import net.minecraft.client.gui.hud.InGameHud; import net.minecraft.client.render.RenderTickCounter; +import net.minecraft.client.texture.NativeImage; +import net.minecraft.client.texture.NativeImageBackedTexture; import net.minecraft.entity.LivingEntity; import net.minecraft.util.Identifier; import net.minecraft.util.profiler.Profilers; +import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.Unique; @@ -15,6 +19,10 @@ import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import java.io.InputStream; +import java.nio.file.Path; + +import static de.rettichlp.ucutils.UCUtils.LOGGER; import static de.rettichlp.ucutils.UCUtils.MOD_ID; import static de.rettichlp.ucutils.UCUtils.configuration; import static de.rettichlp.ucutils.UCUtils.nameTagService; @@ -26,6 +34,7 @@ import static java.lang.Math.clamp; import static java.lang.Math.round; import static java.lang.System.currentTimeMillis; +import static java.nio.file.Files.newInputStream; import static net.minecraft.client.gl.RenderPipelines.GUI_TEXTURED; import static net.minecraft.item.Items.GOLDEN_HOE; import static net.minecraft.registry.tag.FluidTags.WATER; @@ -43,9 +52,40 @@ public abstract class InGameHudMixin { @Unique private static final Identifier HYDRATION_FULL_TEXTURE = Identifier.of(MOD_ID, "textures/hud/hydration_full.png"); + @Unique + private static final Identifier CAPTCHA_IDENTIFIER = Identifier.of("ucutils", "captcha"); + + @Shadow + @Final + private MinecraftClient client; + @Shadow public abstract TextRenderer getTextRenderer(); + @Inject(method = "renderChat", at = @At("TAIL")) + private void ucutils$renderChatTail(DrawContext context, RenderTickCounter tickCounter, CallbackInfo ci) { + if (storage.getCaptchaMap() == null) { + return; + } + + NativeImage nativeImage; + try (InputStream is = newInputStream(Path.of("captcha.png"))) { + nativeImage = NativeImage.read(is); + } catch (Exception e) { + LOGGER.error("Failed to read captcha image", e); + return; + } + + NativeImageBackedTexture nativeImageBackedTexture = new NativeImageBackedTexture(() -> "captcha", nativeImage); + this.client.getTextureManager().registerTexture(CAPTCHA_IDENTIFIER, nativeImageBackedTexture); + + int side = context.getScaledWindowWidth() / 4; + int x = context.getScaledWindowWidth() / 2 - side / 2; + int y = context.getScaledWindowHeight() / 4 - side / 2; + + context.drawTexture(GUI_TEXTURED, CAPTCHA_IDENTIFIER, x, y, 0, 0, side, side, side, side); + } + @Inject(method = "renderCrosshair", at = @At( value = "INVOKE",