diff options
Diffstat (limited to 'src/main/java/de/hysky/skyblocker/utils')
26 files changed, 2304 insertions, 0 deletions
diff --git a/src/main/java/de/hysky/skyblocker/utils/Boxes.java b/src/main/java/de/hysky/skyblocker/utils/Boxes.java new file mode 100644 index 00000000..cd944a46 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/Boxes.java @@ -0,0 +1,50 @@ +package de.hysky.skyblocker.utils; + +import net.minecraft.util.math.Box; +import net.minecraft.util.math.Direction.Axis; +import net.minecraft.util.math.Vec3d; + +public class Boxes { + /** Returns the vector of the min pos of this box. **/ + public static Vec3d getMinVec(Box box) { + return new Vec3d(box.minX, box.minY, box.minZ); + } + + /** Returns the vector of the max pos of this box. **/ + public static Vec3d getMaxVec(Box box) { + return new Vec3d(box.maxX, box.maxY, box.maxZ); + } + + /** Returns the vector of the side lengths of this box. **/ + public static Vec3d getLengthVec(Box box) { + return new Vec3d(box.getLengthX(), box.getLengthY(), box.getLengthZ()); + } + + /** Offsets this box so that minX, minY and minZ are all zero. **/ + public static Box moveToZero(Box box) { + return box.offset(getMinVec(box).negate()); + } + + /** Returns the distance between to oppisite corners of the box. **/ + public static double getCornerLength(Box box) { + return getMinVec(box).distanceTo(getMaxVec(box)); + } + + /** Returns the length of an axis in the box. **/ + public static double getAxisLength(Box box, Axis axis) { + return box.getMax(axis) - box.getMin(axis); + } + + /** Returns a box with each axis multiplied by the amount specified. **/ + public static Box multiply(Box box, double amount) { + return multiply(box, amount, amount, amount); + } + + /** Returns a box with each axis multiplied by the amount specified. **/ + public static Box multiply(Box box, double x, double y, double z) { + return box.expand( + getAxisLength(box, Axis.X) * (x - 1) / 2d, + getAxisLength(box, Axis.Y) * (y - 1) / 2d, + getAxisLength(box, Axis.Z) * (z - 1) / 2d); + } +} diff --git a/src/main/java/de/hysky/skyblocker/utils/Constants.java b/src/main/java/de/hysky/skyblocker/utils/Constants.java new file mode 100644 index 00000000..fbeb448c --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/Constants.java @@ -0,0 +1,8 @@ +package de.hysky.skyblocker.utils; + +/** + * Holds generic static constants + */ +public interface Constants { + String LEVEL_EMBLEMS = "\u2E15\u273F\u2741\u2E19\u03B1\u270E\u2615\u2616\u2663\u213B\u2694\u27B6\u26A1\u2604\u269A\u2693\u2620\u269B\u2666\u2660\u2764\u2727\u238A\u1360\u262C\u269D\u29C9\uA214\u32D6\u2E0E\u26A0\uA541\u3020\u30C4\u2948\u2622\u2623\u273E\u269C\u0BD0\u0A6D\u2742\u16C3\u3023\u10F6\u0444\u266A\u266B\u04C3\u26C1\u26C3\u16DD\uA03E\u1C6A\u03A3\u09EB\u2603\u2654\u26C2\u12DE"; +} diff --git a/src/main/java/de/hysky/skyblocker/utils/Http.java b/src/main/java/de/hysky/skyblocker/utils/Http.java new file mode 100644 index 00000000..ee500b5a --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/Http.java @@ -0,0 +1,89 @@ +package de.hysky.skyblocker.utils; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Version; +import java.net.http.HttpHeaders; +import java.net.http.HttpRequest; +import java.net.http.HttpRequest.BodyPublishers; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.time.Duration; +import java.util.zip.GZIPInputStream; +import java.util.zip.InflaterInputStream; + +import de.hysky.skyblocker.SkyblockerMod; +import net.minecraft.SharedConstants; + +/** + * @implNote All http requests are sent using HTTP 2 + */ +public class Http { + private static final String USER_AGENT = "Skyblocker/" + SkyblockerMod.VERSION + " (" + SharedConstants.getGameVersion().getName() + ")"; + private static final HttpClient HTTP_CLIENT = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(10)) + .build(); + + public static String sendGetRequest(String url) throws IOException, InterruptedException { + HttpRequest request = HttpRequest.newBuilder() + .GET() + .header("Accept", "application/json") + .header("Accept-Encoding", "gzip, deflate") + .header("User-Agent", USER_AGENT) + .version(Version.HTTP_2) + .uri(URI.create(url)) + .build(); + + HttpResponse<InputStream> response = HTTP_CLIENT.send(request, BodyHandlers.ofInputStream()); + InputStream decodedInputStream = getDecodedInputStream(response); + String body = new String(decodedInputStream.readAllBytes()); + + return body; + } + + public static HttpHeaders sendHeadRequest(String url) throws IOException, InterruptedException { + HttpRequest request = HttpRequest.newBuilder() + .method("HEAD", BodyPublishers.noBody()) + .header("User-Agent", USER_AGENT) + .version(Version.HTTP_2) + .uri(URI.create(url)) + .build(); + + HttpResponse<Void> response = HTTP_CLIENT.send(request, BodyHandlers.discarding()); + return response.headers(); + } + + private static InputStream getDecodedInputStream(HttpResponse<InputStream> response) { + String encoding = getContentEncoding(response); + + try { + switch (encoding) { + case "": + return response.body(); + case "gzip": + return new GZIPInputStream(response.body()); + case "deflate": + return new InflaterInputStream(response.body()); + default: + throw new UnsupportedOperationException("The server sent content in an unexpected encoding: " + encoding); + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private static String getContentEncoding(HttpResponse<InputStream> response) { + return response.headers().firstValue("Content-Encoding").orElse(""); + } + + public static String getEtag(HttpHeaders headers) { + return headers.firstValue("Etag").orElse(""); + } + + public static String getLastModified(HttpHeaders headers) { + return headers.firstValue("Last-Modified").orElse(""); + } +} diff --git a/src/main/java/de/hysky/skyblocker/utils/ItemUtils.java b/src/main/java/de/hysky/skyblocker/utils/ItemUtils.java new file mode 100644 index 00000000..6ae1b4d0 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/ItemUtils.java @@ -0,0 +1,111 @@ +package de.hysky.skyblocker.utils; + +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.item.TooltipContext; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.nbt.StringNbtReader; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.regex.Pattern; + +public class ItemUtils { + private final static Pattern WHITESPACES = Pattern.compile("^\\s*$"); + + public static List<Text> getTooltip(ItemStack item) { + MinecraftClient client = MinecraftClient.getInstance(); + return client.player == null || item == null ? Collections.emptyList() : item.getTooltip(client.player, TooltipContext.Default.BASIC); + } + + public static List<String> getTooltipStrings(ItemStack item) { + List<Text> lines = getTooltip(item); + List<String> list = new ArrayList<>(); + + for (Text line : lines) { + String string = line.getString(); + if (!WHITESPACES.matcher(string).matches()) + list.add(string); + } + + return list; + } + + @Nullable + public static Durability getDurability(ItemStack stack) { + if (!Utils.isOnSkyblock() || !SkyblockerConfigManager.get().locations.dwarvenMines.enableDrillFuel || stack.isEmpty()) { + return null; + } + + NbtCompound tag = stack.getNbt(); + if (tag == null || !tag.contains("ExtraAttributes")) { + return null; + } + + NbtCompound extraAttributes = tag.getCompound("ExtraAttributes"); + if (!extraAttributes.contains("drill_fuel") && !extraAttributes.getString("id").equals("PICKONIMBUS")) { + return null; + } + + int current = 0; + int max = 0; + String clearFormatting; + + for (String line : ItemUtils.getTooltipStrings(stack)) { + clearFormatting = Formatting.strip(line); + if (line.contains("Fuel: ")) { + if (clearFormatting != null) { + String clear = Pattern.compile("[^0-9 /]").matcher(clearFormatting).replaceAll("").trim(); + String[] split = clear.split("/"); + current = Integer.parseInt(split[0]); + max = Integer.parseInt(split[1]) * 1000; + return new Durability(current, max); + } + } else if (line.contains("uses.")) { + if (clearFormatting != null) { + int startIndex = clearFormatting.lastIndexOf("after") + 6; + int endIndex = clearFormatting.indexOf("uses", startIndex); + if (startIndex >= 0 && endIndex > startIndex) { + String usesString = clearFormatting.substring(startIndex, endIndex).trim(); + current = Integer.parseInt(usesString); + max = 5000; + } + return new Durability(current, max); + } + } + } + + return null; + } + + public static ItemStack getSkyblockerStack() { + try { + return ItemStack.fromNbt(StringNbtReader.parse("{id:\"minecraft:player_head\",Count:1,tag:{SkullOwner:{Id:[I;-300151517,-631415889,-1193921967,-1821784279],Properties:{textures:[{Value:\"e3RleHR1cmVzOntTS0lOOnt1cmw6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZDdjYzY2ODc0MjNkMDU3MGQ1NTZhYzUzZTA2NzZjYjU2M2JiZGQ5NzE3Y2Q4MjY5YmRlYmVkNmY2ZDRlN2JmOCJ9fX0=\"}]}}}}")); + } catch (CommandSyntaxException e) { + throw new RuntimeException(e); + } + } + + public static String getItemId(ItemStack itemStack) { + if (itemStack == null) return null; + + NbtCompound nbt = itemStack.getNbt(); + if (nbt != null && nbt.contains("ExtraAttributes")) { + NbtCompound extraAttributes = nbt.getCompound("ExtraAttributes"); + if (extraAttributes.contains("id")) { + return extraAttributes.getString("id"); + } + } + + return null; + } + + public record Durability(int current, int max) { + } +} diff --git a/src/main/java/de/hysky/skyblocker/utils/NEURepo.java b/src/main/java/de/hysky/skyblocker/utils/NEURepo.java new file mode 100644 index 00000000..9bc6b245 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/NEURepo.java @@ -0,0 +1,101 @@ +package de.hysky.skyblocker.utils; + +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.skyblock.itemlist.ItemRegistry; +import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager; +import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; +import net.minecraft.client.MinecraftClient; +import net.minecraft.text.Text; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.TransportException; +import org.eclipse.jgit.errors.RepositoryNotFoundException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +/** + * Initializes the NEU repo, which contains item metadata and fairy souls location data. Clones the repo if it does not exist and checks for updates. Use {@link #runAsyncAfterLoad(Runnable)} to run code after the repo is initialized. + */ +public class NEURepo { + private static final Logger LOGGER = LoggerFactory.getLogger(NEURepo.class); + public static final String REMOTE_REPO_URL = "https://github.com/NotEnoughUpdates/NotEnoughUpdates-REPO.git"; + public static final Path LOCAL_REPO_DIR = SkyblockerMod.CONFIG_DIR.resolve("item-repo"); + private static final CompletableFuture<Void> REPO_INITIALIZED = initRepository(); + + /** + * Adds command to update repository manually from ingame. + * <p></p> + * TODO A button could be added to the settings menu that will trigger this command. + */ + public static void init() { + ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> + dispatcher.register(ClientCommandManager.literal(SkyblockerMod.NAMESPACE) + .then(ClientCommandManager.literal("updaterepository").executes(context -> { + deleteAndDownloadRepository(); + return 1; + })))); + } + + private static CompletableFuture<Void> initRepository() { + return CompletableFuture.runAsync(() -> { + try { + if (Files.isDirectory(NEURepo.LOCAL_REPO_DIR)) { + try (Git localRepo = Git.open(NEURepo.LOCAL_REPO_DIR.toFile())) { + localRepo.pull().setRebase(true).call(); + LOGGER.info("[Skyblocker] NEU Repository Updated"); + } + } else { + Git.cloneRepository().setURI(REMOTE_REPO_URL).setDirectory(NEURepo.LOCAL_REPO_DIR.toFile()).setBranchesToClone(List.of("refs/heads/master")).setBranch("refs/heads/master").call().close(); + LOGGER.info("[Skyblocker] NEU Repository Downloaded"); + } + } catch (TransportException e){ + LOGGER.error("[Skyblocker] Transport operation failed. Most likely unable to connect to the remote NEU repo on github", e); + } catch (RepositoryNotFoundException e) { + LOGGER.warn("[Skyblocker] Local NEU Repository not found or corrupted, downloading new one", e); + deleteAndDownloadRepository(); + } catch (Exception e) { + LOGGER.error("[Skyblocker] Encountered unknown exception while initializing NEU Repository", e); + } + }); + } + + private static void deleteAndDownloadRepository() { + CompletableFuture.runAsync(() -> { + try { + ItemRegistry.filesImported = false; + File dir = NEURepo.LOCAL_REPO_DIR.toFile(); + recursiveDelete(dir); + } catch (Exception ex) { + if (MinecraftClient.getInstance().player != null) + MinecraftClient.getInstance().player.sendMessage(Text.translatable("skyblocker.updaterepository.failed"), false); + return; + } + initRepository(); + }); + } + + @SuppressWarnings("ResultOfMethodCallIgnored") + private static void recursiveDelete(File dir) { + File[] children; + if (dir.isDirectory() && !Files.isSymbolicLink(dir.toPath()) && (children = dir.listFiles()) != null) { + for (File child : children) { + recursiveDelete(child); + } + } + dir.delete(); + } + + /** + * Runs the given runnable after the NEU repo is initialized. + * @param runnable the runnable to run + * @return a completable future of the given runnable + */ + public static CompletableFuture<Void> runAsyncAfterLoad(Runnable runnable) { + return REPO_INITIALIZED.thenRunAsync(runnable); + } +} diff --git a/src/main/java/de/hysky/skyblocker/utils/PosUtils.java b/src/main/java/de/hysky/skyblocker/utils/PosUtils.java new file mode 100644 index 00000000..6a34b060 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/PosUtils.java @@ -0,0 +1,14 @@ +package de.hysky.skyblocker.utils; + +import net.minecraft.util.math.BlockPos; + +public final class PosUtils { + public static BlockPos parsePosString(String posData) { + String[] posArray = posData.split(","); + return new BlockPos(Integer.parseInt(posArray[0]), Integer.parseInt(posArray[1]), Integer.parseInt(posArray[2])); + } + + public static String getPosString(BlockPos blockPos) { + return blockPos.getX() + "," + blockPos.getY() + "," + blockPos.getZ(); + } +} diff --git a/src/main/java/de/hysky/skyblocker/utils/SlayerUtils.java b/src/main/java/de/hysky/skyblocker/utils/SlayerUtils.java new file mode 100644 index 00000000..0a42c6ae --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/SlayerUtils.java @@ -0,0 +1,54 @@ +package de.hysky.skyblocker.utils; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.entity.Entity; +import net.minecraft.entity.decoration.ArmorStandEntity; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; + +//TODO Slayer Packet system that can provide information about the current slayer boss, abstract so that different bosses can have different info +public class SlayerUtils { + private static final Logger LOGGER = LoggerFactory.getLogger(SlayerUtils.class); + + //TODO: Cache this, probably included in Packet system + public static List<Entity> getEntityArmorStands(Entity entity) { + return entity.getEntityWorld().getOtherEntities(entity, entity.getBoundingBox().expand(1F, 2.5F, 1F), x -> x instanceof ArmorStandEntity && x.hasCustomName()); + } + + //Eventually this should be modified so that if you hit a slayer boss all slayer features will work on that boss. + public static Entity getSlayerEntity() { + if (MinecraftClient.getInstance().world != null) { + for (Entity entity : MinecraftClient.getInstance().world.getEntities()) { + //Check if entity is Bloodfiend + if (entity.hasCustomName() && entity.getCustomName().getString().contains("Bloodfiend")) { + //Grab the players username + String username = MinecraftClient.getInstance().getSession().getUsername(); + //Check all armor stands around the boss + for (Entity armorStand : getEntityArmorStands(entity)) { + //Check if the display name contains the players username + if (armorStand.getDisplayName().getString().contains(username)) { + return entity; + } + } + } + } + } + return null; + } + + public static boolean isInSlayer() { + try { + for (int i = 0; i < Utils.STRING_SCOREBOARD.size(); i++) { + String line = Utils.STRING_SCOREBOARD.get(i); + + if (line.contains("Slay the boss!")) return true; + } + } catch (NullPointerException e) { + LOGGER.error("[Skyblocker] Error while checking if player is in slayer", e); + } + + return false; + } +}
\ No newline at end of file diff --git a/src/main/java/de/hysky/skyblocker/utils/Utils.java b/src/main/java/de/hysky/skyblocker/utils/Utils.java new file mode 100644 index 00000000..f046bffb --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/Utils.java @@ -0,0 +1,370 @@ +package de.hysky.skyblocker.utils; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import de.hysky.skyblocker.events.SkyblockEvents; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import de.hysky.skyblocker.skyblock.item.PriceInfoTooltip; +import de.hysky.skyblocker.skyblock.rift.TheRift; +import de.hysky.skyblocker.utils.scheduler.MessageScheduler; +import net.fabricmc.fabric.api.client.item.v1.ItemTooltipCallback; +import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; +import net.fabricmc.fabric.api.networking.v1.PacketSender; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ClientPlayNetworkHandler; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.client.network.PlayerListEntry; +import net.minecraft.scoreboard.Scoreboard; +import net.minecraft.scoreboard.ScoreboardDisplaySlot; +import net.minecraft.scoreboard.ScoreboardObjective; +import net.minecraft.scoreboard.ScoreboardPlayerScore; +import net.minecraft.scoreboard.Team; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collections; +import java.util.List; + +/** + * Utility variables and methods for retrieving Skyblock related information. + */ +public class Utils { + private static final Logger LOGGER = LoggerFactory.getLogger(Utils.class); + private static final String ALTERNATE_HYPIXEL_ADDRESS = System.getProperty("skyblocker.alternateHypixelAddress", ""); + private static final String PROFILE_PREFIX = "Profile: "; + private static boolean isOnHypixel = false; + private static boolean isOnSkyblock = false; + private static boolean isInDungeons = false; + private static boolean isInjected = false; + /** + * The following fields store data returned from /locraw: {@link #profile}, {@link #server}, {@link #gameType}, {@link #locationRaw}, and {@link #map}. + */ + @SuppressWarnings("JavadocDeclaration") + private static String profile = ""; + private static String server = ""; + private static String gameType = ""; + private static String locationRaw = ""; + private static String map = ""; + private static long clientWorldJoinTime = 0; + private static boolean sentLocRaw = false; + private static boolean canSendLocRaw = false; + + /** + * @implNote The parent text will always be empty, the actual text content is inside the text's siblings. + */ + public static final ObjectArrayList<Text> TEXT_SCOREBOARD = new ObjectArrayList<>(); + public static final ObjectArrayList<String> STRING_SCOREBOARD = new ObjectArrayList<>(); + + public static boolean isOnHypixel() { + return isOnHypixel; + } + + public static boolean isOnSkyblock() { + return isOnSkyblock; + } + + public static boolean isInDungeons() { + return isInDungeons; + } + + public static boolean isInTheRift() { + return getLocationRaw().equals(TheRift.LOCATION); + } + + public static boolean isInjected() { + return isInjected; + } + + /** + * @return the profile parsed from the player list. + */ + public static String getProfile() { + return profile; + } + + /** + * @return the server parsed from /locraw. + */ + public static String getServer() { + return server; + } + + /** + * @return the game type parsed from /locraw. + */ + public static String getGameType() { + return gameType; + } + + /** + * @return the location raw parsed from /locraw. + */ + public static String getLocationRaw() { + return locationRaw; + } + + /** + * @return the map parsed from /locraw. + */ + public static String getMap() { + return map; + } + + public static void init() { + ClientPlayConnectionEvents.JOIN.register(Utils::onClientWorldJoin); + ClientReceiveMessageEvents.ALLOW_GAME.register(Utils::onChatMessage); + ClientReceiveMessageEvents.GAME_CANCELED.register(Utils::onChatMessage); // Somehow this works even though onChatMessage returns a boolean + } + + /** + * Updates all the fields stored in this class from the sidebar, player list, and /locraw. + */ + public static void update() { + MinecraftClient client = MinecraftClient.getInstance(); + updateScoreboard(client); + updatePlayerPresenceFromScoreboard(client); + updateFromPlayerList(client); + updateLocRaw(); + } + + /** + * Updates {@link #isOnSkyblock}, {@link #isInDungeons}, and {@link #isInjected} from the scoreboard. + */ + public static void updatePlayerPresenceFromScoreboard(MinecraftClient client) { + List<String> sidebar = STRING_SCOREBOARD; + + FabricLoader fabricLoader = FabricLoader.getInstance(); + if ((client.world == null || client.isInSingleplayer() || sidebar == null || sidebar.isEmpty())) { + if (fabricLoader.isDevelopmentEnvironment()) { + sidebar = Collections.emptyList(); + } else { + isOnSkyblock = false; + isInDungeons = false; + return; + } + } + + if (sidebar.isEmpty() && !fabricLoader.isDevelopmentEnvironment()) return; + String string = sidebar.toString(); + + if (fabricLoader.isDevelopmentEnvironment() || isConnectedToHypixel(client)) { + if (!isOnHypixel) { + isOnHypixel = true; + } + if (fabricLoader.isDevelopmentEnvironment() || sidebar.get(0).contains("SKYBLOCK") || sidebar.get(0).contains("SKIBLOCK")) { + if (!isOnSkyblock) { + if (!isInjected) { + isInjected = true; + ItemTooltipCallback.EVENT.register(PriceInfoTooltip::onInjectTooltip); + } + isOnSkyblock = true; + SkyblockEvents.JOIN.invoker().onSkyblockJoin(); + } + } else { + onLeaveSkyblock(); + } + isInDungeons = fabricLoader.isDevelopmentEnvironment() || isOnSkyblock && string.contains("The Catacombs"); + } else if (isOnHypixel) { + isOnHypixel = false; + onLeaveSkyblock(); + } + } + + private static boolean isConnectedToHypixel(MinecraftClient client) { + String serverAddress = (client.getCurrentServerEntry() != null) ? client.getCurrentServerEntry().address.toLowerCase() : ""; + String serverBrand = (client.player != null && client.player.networkHandler != null && client.player.networkHandler.getBrand() != null) ? client.player.networkHandler.getBrand() : ""; + + return serverAddress.equalsIgnoreCase(ALTERNATE_HYPIXEL_ADDRESS) || serverAddress.contains("hypixel.net") || serverAddress.contains("hypixel.io") || serverBrand.contains("Hypixel BungeeCord"); + } + + private static void onLeaveSkyblock() { + if (isOnSkyblock) { + isOnSkyblock = false; + isInDungeons = false; + SkyblockEvents.LEAVE.invoker().onSkyblockLeave(); + } + } + + public static String getLocation() { + String location = null; + List<String> sidebarLines = STRING_SCOREBOARD; + try { + if (sidebarLines != null) { + for (String sidebarLine : sidebarLines) { + if (sidebarLine.contains("⏣")) location = sidebarLine; + if (sidebarLine.contains("ф")) location = sidebarLine; //Rift + } + if (location == null) location = "Unknown"; + location = location.strip(); + } + } catch (IndexOutOfBoundsException e) { + LOGGER.error("[Skyblocker] Failed to get location from sidebar", e); + } + return location; + } + + public static double getPurse() { + String purseString = null; + double purse = 0; + + List<String> sidebarLines = STRING_SCOREBOARD; + try { + + if (sidebarLines != null) { + for (String sidebarLine : sidebarLines) { + if (sidebarLine.contains("Piggy:")) purseString = sidebarLine; + if (sidebarLine.contains("Purse:")) purseString = sidebarLine; + } + } + if (purseString != null) purse = Double.parseDouble(purseString.replaceAll("[^0-9.]", "").strip()); + else purse = 0; + + } catch (IndexOutOfBoundsException e) { + LOGGER.error("[Skyblocker] Failed to get purse from sidebar", e); + } + return purse; + } + + public static int getBits() { + int bits = 0; + String bitsString = null; + List<String> sidebarLines = STRING_SCOREBOARD; + try { + if (sidebarLines != null) { + for (String sidebarLine : sidebarLines) { + if (sidebarLine.contains("Bits")) bitsString = sidebarLine; + } + } + if (bitsString != null) { + bits = Integer.parseInt(bitsString.replaceAll("[^0-9.]", "").strip()); + } + } catch (IndexOutOfBoundsException e) { + LOGGER.error("[Skyblocker] Failed to get bits from sidebar", e); + } + return bits; + } + + private static void updateScoreboard(MinecraftClient client) { + try { + TEXT_SCOREBOARD.clear(); + STRING_SCOREBOARD.clear(); + + ClientPlayerEntity player = client.player; + if (player == null) return; + + Scoreboard scoreboard = player.getScoreboard(); + ScoreboardObjective objective = scoreboard.getObjectiveForSlot(ScoreboardDisplaySlot.FROM_ID.apply(1)); + ObjectArrayList<Text> textLines = new ObjectArrayList<>(); + ObjectArrayList<String> stringLines = new ObjectArrayList<>(); + + for (ScoreboardPlayerScore score : scoreboard.getAllPlayerScores(objective)) { + Team team = scoreboard.getPlayerTeam(score.getPlayerName()); + + if (team != null) { + Text textLine = Text.empty().append(team.getPrefix().copy()).append(team.getSuffix().copy()); + String strLine = team.getPrefix().getString() + team.getSuffix().getString(); + + if (!strLine.trim().isEmpty()) { + String formatted = Formatting.strip(strLine); + + textLines.add(textLine); + stringLines.add(formatted); + } + } + } + + if (objective != null) { + stringLines.add(objective.getDisplayName().getString()); + textLines.add(Text.empty().append(objective.getDisplayName().copy())); + + Collections.reverse(stringLines); + Collections.reverse(textLines); + } + + TEXT_SCOREBOARD.addAll(textLines); + STRING_SCOREBOARD.addAll(stringLines); + } catch (NullPointerException e) { + //Do nothing + } + } + + private static void updateFromPlayerList(MinecraftClient client) { + if (client.getNetworkHandler() == null) { + return; + } + for (PlayerListEntry playerListEntry : client.getNetworkHandler().getPlayerList()) { + if (playerListEntry.getDisplayName() == null) { + continue; + } + String name = playerListEntry.getDisplayName().getString(); + if (name.startsWith(PROFILE_PREFIX)) { + profile = name.substring(PROFILE_PREFIX.length()); + } + } + } + + public static void onClientWorldJoin(ClientPlayNetworkHandler handler, PacketSender sender, MinecraftClient client) { + clientWorldJoinTime = System.currentTimeMillis(); + resetLocRawInfo(); + } + + /** + * Sends /locraw to the server if the player is on skyblock and on a new island. + */ + private static void updateLocRaw() { + if (isOnSkyblock) { + long currentTime = System.currentTimeMillis(); + if (!sentLocRaw && canSendLocRaw && currentTime > clientWorldJoinTime + 1000) { + MessageScheduler.INSTANCE.sendMessageAfterCooldown("/locraw"); + sentLocRaw = true; + canSendLocRaw = false; + } + } else { + resetLocRawInfo(); + } + } + + /** + * Parses the /locraw reply from the server + * + * @return not display the message in chat is the command is sent by the mod + */ + public static boolean onChatMessage(Text text, boolean overlay) { + String message = text.getString(); + if (message.startsWith("{\"server\":") && message.endsWith("}")) { + JsonObject locRaw = JsonParser.parseString(message).getAsJsonObject(); + if (locRaw.has("server")) { + server = locRaw.get("server").getAsString(); + if (locRaw.has("gameType")) { + gameType = locRaw.get("gameType").getAsString(); + } + if (locRaw.has("mode")) { + locationRaw = locRaw.get("mode").getAsString(); + } + if (locRaw.has("map")) { + map = locRaw.get("map").getAsString(); + } + + boolean shouldFilter = !sentLocRaw; + sentLocRaw = false; + + return shouldFilter; + } + } + return true; + } + + private static void resetLocRawInfo() { + sentLocRaw = false; + canSendLocRaw = true; + server = ""; + gameType = ""; + locationRaw = ""; + map = ""; + } +} diff --git a/src/main/java/de/hysky/skyblocker/utils/chat/ChatFilterResult.java b/src/main/java/de/hysky/skyblocker/utils/chat/ChatFilterResult.java new file mode 100644 index 00000000..5a94682a --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/chat/ChatFilterResult.java @@ -0,0 +1,18 @@ +package de.hysky.skyblocker.utils.chat; + +import net.minecraft.client.resource.language.I18n; +public enum ChatFilterResult { + // Skip this one / no action + PASS, + // Filter + FILTER, + // Move to action bar + ACTION_BAR; + // Skip remaining checks, don't filter + // null + + @Override + public String toString() { + return I18n.translate("text.autoconfig.skyblocker.option.messages.chatFilterResult." + name()); + } +} diff --git a/src/main/java/de/hysky/skyblocker/utils/chat/ChatMessageListener.java b/src/main/java/de/hysky/skyblocker/utils/chat/ChatMessageListener.java new file mode 100644 index 00000000..7892445e --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/chat/ChatMessageListener.java @@ -0,0 +1,89 @@ +package de.hysky.skyblocker.utils.chat; + +import de.hysky.skyblocker.skyblock.filters.*; +import de.hysky.skyblocker.utils.Utils; +import de.hysky.skyblocker.skyblock.barn.HungryHiker; +import de.hysky.skyblocker.skyblock.barn.TreasureHunter; +import de.hysky.skyblocker.skyblock.dungeon.Reparty; +import de.hysky.skyblocker.skyblock.dungeon.ThreeWeirdos; +import de.hysky.skyblocker.skyblock.dungeon.Trivia; +import de.hysky.skyblocker.skyblock.dwarven.Fetchur; +import de.hysky.skyblocker.skyblock.dwarven.Puzzler; +import de.hysky.skyblocker.skyblock.filters.*; +import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents; +import net.fabricmc.fabric.api.event.Event; +import net.fabricmc.fabric.api.event.EventFactory; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.text.Text; + +@FunctionalInterface +public interface ChatMessageListener { + /** + * An event called when a game message is received. Register your listeners in {@link ChatMessageListener#init()}. + */ + Event<ChatMessageListener> EVENT = EventFactory.createArrayBacked(ChatMessageListener.class, + (listeners) -> (message, asString) -> { + for (ChatMessageListener listener : listeners) { + ChatFilterResult result = listener.onMessage(message, asString); + if (result != ChatFilterResult.PASS) return result; + } + return ChatFilterResult.PASS; + }); + + /** + * Registers {@link ChatMessageListener}s to {@link ChatMessageListener#EVENT} and registers {@link ChatMessageListener#EVENT} to {@link ClientReceiveMessageEvents#ALLOW_GAME} + */ + static void init() { + ChatMessageListener[] listeners = new ChatMessageListener[]{ + // Features + new Fetchur(), + new Puzzler(), + new Reparty(), + new ThreeWeirdos(), + new Trivia(), + new TreasureHunter(), + new HungryHiker(), + // Filters + new AbilityFilter(), + new AdFilter(), + new AoteFilter(), + new ComboFilter(), + new HealFilter(), + new ImplosionFilter(), + new MoltenWaveFilter(), + new TeleportPadFilter(), + new AutopetFilter(), + new ShowOffFilter() + }; + // Register all listeners to EVENT + for (ChatMessageListener listener : listeners) { + EVENT.register(listener); + } + // Register EVENT to ClientReceiveMessageEvents.ALLOW_GAME from fabric api + ClientReceiveMessageEvents.ALLOW_GAME.register((message, overlay) -> { + if (!Utils.isOnSkyblock()) { + return true; + } + ChatFilterResult result = EVENT.invoker().onMessage(message, message.getString()); + switch (result) { + case ACTION_BAR -> { + if (overlay) { + return true; + } + ClientPlayerEntity player = MinecraftClient.getInstance().player; + if (player != null) { + player.sendMessage(message, true); + return false; + } + } + case FILTER -> { + return false; + } + } + return true; + }); + } + + ChatFilterResult onMessage(Text message, String asString); +} diff --git a/src/main/java/de/hysky/skyblocker/utils/chat/ChatPatternListener.java b/src/main/java/de/hysky/skyblocker/utils/chat/ChatPatternListener.java new file mode 100644 index 00000000..708af280 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/chat/ChatPatternListener.java @@ -0,0 +1,30 @@ +package de.hysky.skyblocker.utils.chat; + +import net.minecraft.text.Text; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public abstract class ChatPatternListener implements ChatMessageListener { + protected static final String NUMBER = "-?[0-9]{1,3}(?>,[0-9]{3})*(?:\\.[1-9])?"; + public final Pattern pattern; + + public ChatPatternListener(String pattern) { + this.pattern = Pattern.compile(pattern); + } + + @Override + public final ChatFilterResult onMessage(Text message, String asString) { + ChatFilterResult state = state(); + if (state == ChatFilterResult.PASS) return ChatFilterResult.PASS; + Matcher m = pattern.matcher(asString); + if (m.matches() && onMatch(message, m)) { + return state; + } + return ChatFilterResult.PASS; + } + + protected abstract ChatFilterResult state(); + + protected abstract boolean onMatch(Text message, Matcher matcher); +} diff --git a/src/main/java/de/hysky/skyblocker/utils/discord/DiscordRPCManager.java b/src/main/java/de/hysky/skyblocker/utils/discord/DiscordRPCManager.java new file mode 100644 index 00000000..f0589a0b --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/discord/DiscordRPCManager.java @@ -0,0 +1,122 @@ +package de.hysky.skyblocker.utils.discord; + + +import de.hysky.skyblocker.config.SkyblockerConfig; +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.events.SkyblockEvents; +import de.hysky.skyblocker.utils.Utils; +import meteordevelopment.discordipc.DiscordIPC; +import meteordevelopment.discordipc.RichPresence; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.text.DecimalFormat; +import java.util.concurrent.CompletableFuture; + +/** + * Manages the discord rich presence. Automatically connects to discord and displays a customizable activity when playing Skyblock. + */ +public class DiscordRPCManager { + public static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("###,###.##"); + public static final Logger LOGGER = LoggerFactory.getLogger("Skyblocker Discord RPC"); + /** + * The update task used to avoid multiple update tasks running simultaneously. + */ + public static CompletableFuture<Void> updateTask; + public static long startTimeStamp; + public static int cycleCount; + + public static void init() { + SkyblockEvents.LEAVE.register(DiscordRPCManager::initAndUpdatePresence); + SkyblockEvents.JOIN.register(() -> { + startTimeStamp = System.currentTimeMillis(); + initAndUpdatePresence(true); + }); + } + + /** + * Checks the {@link SkyblockerConfig.RichPresence#customMessage custom message}, updates {@link #cycleCount} if enabled, and updates rich presence. + */ + public static void updateDataAndPresence() { + // If the custom message is empty, discord will keep the last message, this is can serve as a default if the user doesn't want a custom message + if (SkyblockerConfigManager.get().richPresence.customMessage.isEmpty()) { + SkyblockerConfigManager.get().richPresence.customMessage = "Playing Skyblock"; + SkyblockerConfigManager.save(); + } + if (SkyblockerConfigManager.get().richPresence.cycleMode) cycleCount = (cycleCount + 1) % 3; + initAndUpdatePresence(); + } + + /** + * @see #initAndUpdatePresence(boolean) + */ + private static void initAndUpdatePresence() { + initAndUpdatePresence(false); + } + + /** + * Updates discord presence asynchronously. + * <p> + * When the {@link #updateTask previous update} does not exist or {@link CompletableFuture#isDone() has completed}: + * <p> + * Connects to discord if {@link SkyblockerConfig.RichPresence#enableRichPresence rich presence is enabled}, + * the player {@link Utils#isOnSkyblock() is on Skyblock}, and {@link DiscordIPC#isConnected() discord is not already connected}. + * Updates the presence if {@link SkyblockerConfig.RichPresence#enableRichPresence rich presence is enabled} + * and the player {@link Utils#isOnSkyblock() is on Skyblock}. + * Stops the connection if {@link SkyblockerConfig.RichPresence#enableRichPresence rich presence is disabled} + * or the player {@link Utils#isOnSkyblock() is not on Skyblock} and {@link DiscordIPC#isConnected() discord is connected}. + * Saves the update task in {@link #updateTask} + * + * @param initialization whether this is the first time the presence is being updates. If {@code true}, a message will be logged + * if {@link SkyblockerConfig.RichPresence#enableRichPresence rich presence is disabled}. + */ + private static void initAndUpdatePresence(boolean initialization) { + if (updateTask == null || updateTask.isDone()) { + updateTask = CompletableFuture.runAsync(() -> { + if (SkyblockerConfigManager.get().richPresence.enableRichPresence && Utils.isOnSkyblock()) { + if (!DiscordIPC.isConnected()) { + if (DiscordIPC.start(934607927837356052L, null)) { + LOGGER.info("Discord RPC started successfully"); + } else { + LOGGER.error("Discord RPC failed to start"); + return; + } + } + DiscordIPC.setActivity(buildPresence()); + } else if (DiscordIPC.isConnected()) { + DiscordIPC.stop(); + LOGGER.info("Discord RPC stopped"); + } else if (initialization) { + LOGGER.info("Discord RPC is currently disabled"); + } + }); + } + } + + public static RichPresence buildPresence() { + RichPresence presence = new RichPresence(); + presence.setLargeImage("skyblocker-default", null); + presence.setStart(startTimeStamp); + presence.setDetails(SkyblockerConfigManager.get().richPresence.customMessage); + presence.setState(getInfo()); + return presence; + } + + public static String getInfo() { + String info = null; + if (!SkyblockerConfigManager.get().richPresence.cycleMode) { + switch (SkyblockerConfigManager.get().richPresence.info) { + case BITS -> info = "Bits: " + DECIMAL_FORMAT.format(Utils.getBits()); + case PURSE -> info = "Purse: " + DECIMAL_FORMAT.format(Utils.getPurse()); + case LOCATION -> info = Utils.getLocation(); + } + } else if (SkyblockerConfigManager.get().richPresence.cycleMode) { + switch (cycleCount) { + case 0 -> info = "Bits: " + DECIMAL_FORMAT.format(Utils.getBits()); + case 1 -> info = "Purse: " + DECIMAL_FORMAT.format(Utils.getPurse()); + case 2 -> info = Utils.getLocation(); + } + } + return info; + } +} diff --git a/src/main/java/de/hysky/skyblocker/utils/render/FrustumUtils.java b/src/main/java/de/hysky/skyblocker/utils/render/FrustumUtils.java new file mode 100644 index 00000000..3fe79e43 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/render/FrustumUtils.java @@ -0,0 +1,21 @@ +package de.hysky.skyblocker.utils.render; + +import de.hysky.skyblocker.mixin.accessor.FrustumInvoker; +import de.hysky.skyblocker.mixin.accessor.WorldRendererAccessor; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.render.Frustum; +import net.minecraft.util.math.Box; + +public class FrustumUtils { + public static Frustum getFrustum() { + return ((WorldRendererAccessor) MinecraftClient.getInstance().worldRenderer).getFrustum(); + } + + public static boolean isVisible(Box box) { + return getFrustum().isVisible(box); + } + + public static boolean isVisible(double minX, double minY, double minZ, double maxX, double maxY, double maxZ) { + return ((FrustumInvoker) getFrustum()).invokeIsVisible(minX, minY, minZ, maxX, maxY, maxZ); + } +}
\ No newline at end of file diff --git a/src/main/java/de/hysky/skyblocker/utils/render/RenderHelper.java b/src/main/java/de/hysky/skyblocker/utils/render/RenderHelper.java new file mode 100644 index 00000000..4630149c --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/render/RenderHelper.java @@ -0,0 +1,247 @@ +package de.hysky.skyblocker.utils.render; + +import com.mojang.blaze3d.platform.GlStateManager.DstFactor; +import com.mojang.blaze3d.platform.GlStateManager.SrcFactor; +import com.mojang.blaze3d.systems.RenderSystem; +import de.hysky.skyblocker.mixin.accessor.BeaconBlockEntityRendererInvoker; +import me.x150.renderer.render.Renderer3d; +import de.hysky.skyblocker.utils.render.culling.OcclusionCulling; +import de.hysky.skyblocker.utils.render.title.Title; +import de.hysky.skyblocker.utils.render.title.TitleContainer; +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.render.*; +import net.minecraft.client.render.VertexFormat.DrawMode; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.sound.SoundEvents; +import net.minecraft.text.OrderedText; +import net.minecraft.text.Text; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Box; +import net.minecraft.util.math.Vec3d; +import org.joml.Matrix3f; +import org.joml.Matrix4f; +import org.joml.Vector3f; +import org.lwjgl.opengl.GL11; + +import java.awt.*; + +public class RenderHelper { + private static final Vec3d ONE = new Vec3d(1, 1, 1); + private static final int MAX_OVERWORLD_BUILD_HEIGHT = 319; + private static final MinecraftClient client = MinecraftClient.getInstance(); + + public static void renderFilledThroughWallsWithBeaconBeam(WorldRenderContext context, BlockPos pos, float[] colorComponents, float alpha) { + renderFilledThroughWalls(context, pos, colorComponents, alpha); + renderBeaconBeam(context, pos, colorComponents); + } + + public static void renderFilledThroughWalls(WorldRenderContext context, BlockPos pos, float[] colorComponents, float alpha) { + if (FrustumUtils.isVisible(pos.getX(), pos.getY(), pos.getZ(), pos.getX() + 1, pos.getY() + 1, pos.getZ() + 1)) { + Renderer3d.renderThroughWalls(); + renderFilled(context, pos, colorComponents, alpha); + Renderer3d.stopRenderThroughWalls(); + } + } + + public static void renderFilledIfVisible(WorldRenderContext context, BlockPos pos, float[] colorComponents, float alpha) { + if (OcclusionCulling.isVisible(pos.getX(), pos.getY(), pos.getZ(), pos.getX() + 1, pos.getY() + 1, pos.getZ() + 1)) { + renderFilled(context, pos, colorComponents, alpha); + } + } + + private static void renderFilled(WorldRenderContext context, BlockPos pos, float[] colorComponents, float alpha) { + Renderer3d.renderFilled(context.matrixStack(), new Color(colorComponents[0], colorComponents[1], colorComponents[2], alpha), Vec3d.of(pos), ONE); + } + + private static void renderBeaconBeam(WorldRenderContext context, BlockPos pos, float[] colorComponents) { + if (FrustumUtils.isVisible(pos.getX(), pos.getY(), pos.getZ(), pos.getX() + 1, MAX_OVERWORLD_BUILD_HEIGHT, pos.getZ() + 1)) { + MatrixStack matrices = context.matrixStack(); + Vec3d camera = context.camera().getPos(); + + matrices.push(); + matrices.translate(pos.getX() - camera.getX(), pos.getY() - camera.getY(), pos.getZ() - camera.getZ()); + + Tessellator tessellator = RenderSystem.renderThreadTesselator(); + BufferBuilder buffer = tessellator.getBuffer(); + VertexConsumerProvider.Immediate consumer = VertexConsumerProvider.immediate(buffer); + + BeaconBlockEntityRendererInvoker.renderBeam(matrices, consumer, context.tickDelta(), context.world().getTime(), 0, MAX_OVERWORLD_BUILD_HEIGHT, colorComponents); + + consumer.draw(); + matrices.pop(); + } + } + + /** + * Renders the outline of a box with the specified color components and line width. + * This does not use renderer since renderer draws outline using debug lines with a fixed width. + */ + public static void renderOutline(WorldRenderContext context, Box box, float[] colorComponents, float lineWidth) { + if (FrustumUtils.isVisible(box)) { + MatrixStack matrices = context.matrixStack(); + Vec3d camera = context.camera().getPos(); + Tessellator tessellator = RenderSystem.renderThreadTesselator(); + BufferBuilder buffer = tessellator.getBuffer(); + + RenderSystem.setShader(GameRenderer::getRenderTypeLinesProgram); + RenderSystem.setShaderColor(1f, 1f, 1f, 1f); + RenderSystem.lineWidth(lineWidth); + RenderSystem.disableCull(); + RenderSystem.enableDepthTest(); + + matrices.push(); + matrices.translate(-camera.getX(), -camera.getY(), -camera.getZ()); + + buffer.begin(DrawMode.LINES, VertexFormats.LINES); + WorldRenderer.drawBox(matrices, buffer, box, colorComponents[0], colorComponents[1], colorComponents[2], 1f); + tessellator.draw(); + + matrices.pop(); + RenderSystem.lineWidth(1f); + RenderSystem.enableCull(); + RenderSystem.disableDepthTest(); + } + } + + /** + * Draws lines from point to point.<br><br> + * <p> + * Tip: To draw lines from the center of a block, offset the X, Y and Z each by 0.5 + * + * @param context The WorldRenderContext which supplies the matrices and tick delta + * @param points The points from which to draw lines between + * @param colorComponents An array of R, G and B color components + * @param alpha The alpha of the lines + * @param lineWidth The width of the lines + */ + public static void renderLinesFromPoints(WorldRenderContext context, Vec3d[] points, float[] colorComponents, float alpha, float lineWidth) { + Vec3d camera = context.camera().getPos(); + MatrixStack matrices = context.matrixStack(); + + matrices.push(); + matrices.translate(-camera.x, -camera.y, -camera.z); + + Tessellator tessellator = RenderSystem.renderThreadTesselator(); + BufferBuilder buffer = tessellator.getBuffer(); + Matrix4f projectionMatrix = matrices.peek().getPositionMatrix(); + Matrix3f normalMatrix = matrices.peek().getNormalMatrix(); + + GL11.glEnable(GL11.GL_LINE_SMOOTH); + GL11.glHint(GL11.GL_LINE_SMOOTH_HINT, GL11.GL_NICEST); + + RenderSystem.setShader(GameRenderer::getRenderTypeLinesProgram); + RenderSystem.setShaderColor(1f, 1f, 1f, 1f); + RenderSystem.lineWidth(lineWidth); + RenderSystem.enableBlend(); + RenderSystem.blendFunc(SrcFactor.SRC_ALPHA, DstFactor.ONE_MINUS_SRC_ALPHA); + RenderSystem.disableCull(); + RenderSystem.enableDepthTest(); + + buffer.begin(DrawMode.LINE_STRIP, VertexFormats.LINES); + + for (int i = 0; i < points.length; i++) { + Vec3d point = points[i]; + Vec3d nextPoint = (i + 1 == points.length) ? points[i - 1] : points[i + 1]; + Vector3f normalVec = new Vector3f((float) nextPoint.getX(), (float) nextPoint.getY(), (float) nextPoint.getZ()).sub((float) point.getX(), (float) point.getY(), (float) point.getZ()).normalize(); + + buffer + .vertex(projectionMatrix, (float) point.getX(), (float) point.getY(), (float) point.getZ()) + .color(colorComponents[0], colorComponents[1], colorComponents[2], alpha) + .normal(normalMatrix, normalVec.x, normalVec.y, normalVec.z) + .next(); + } + + tessellator.draw(); + + matrices.pop(); + GL11.glDisable(GL11.GL_LINE_SMOOTH); + RenderSystem.lineWidth(1f); + RenderSystem.disableBlend(); + RenderSystem.defaultBlendFunc(); + RenderSystem.enableCull(); + RenderSystem.disableDepthTest(); + } + + public static void renderText(WorldRenderContext context, Text text, Vec3d pos, boolean seeThrough) { + renderText(context, text, pos, 1, seeThrough); + } + + public static void renderText(WorldRenderContext context, Text text, Vec3d pos, float scale, boolean seeThrough) { + renderText(context, text, pos, scale, 0, seeThrough); + } + + public static void renderText(WorldRenderContext context, Text text, Vec3d pos, float scale, float yOffset, boolean seeThrough) { + renderText(context, text.asOrderedText(), pos, scale, yOffset, seeThrough); + } + + /** + * Renders text in the world space. + * + * @param seeThrough Whether the text should be able to be seen through walls or not. + */ + public static void renderText(WorldRenderContext context, OrderedText text, Vec3d pos, float scale, float yOffset, boolean seeThrough) { + MatrixStack matrices = context.matrixStack(); + Vec3d camera = context.camera().getPos(); + TextRenderer textRenderer = client.textRenderer; + + scale *= 0.025f; + + matrices.push(); + matrices.translate(pos.getX() - camera.getX(), pos.getY() - camera.getY(), pos.getZ() - camera.getZ()); + matrices.peek().getPositionMatrix().mul(RenderSystem.getModelViewMatrix()); + matrices.multiply(context.camera().getRotation()); + matrices.scale(-scale, -scale, scale); + + Matrix4f positionMatrix = matrices.peek().getPositionMatrix(); + float xOffset = -textRenderer.getWidth(text) / 2f; + + Tessellator tessellator = RenderSystem.renderThreadTesselator(); + BufferBuilder buffer = tessellator.getBuffer(); + VertexConsumerProvider.Immediate consumers = VertexConsumerProvider.immediate(buffer); + + RenderSystem.depthFunc(seeThrough ? GL11.GL_ALWAYS : GL11.GL_LEQUAL); + + textRenderer.draw(text, xOffset, yOffset, 0xFFFFFFFF, false, positionMatrix, consumers, TextRenderer.TextLayerType.SEE_THROUGH, 0, LightmapTextureManager.MAX_LIGHT_COORDINATE); + consumers.draw(); + + RenderSystem.depthFunc(GL11.GL_LEQUAL); + matrices.pop(); + } + + /** + * Adds the title to {@link TitleContainer} and {@link #playNotificationSound() plays the notification sound} if the title is not in the {@link TitleContainer} already. + * No checking needs to be done on whether the title is in the {@link TitleContainer} already by the caller. + * + * @param title the title + */ + public static void displayInTitleContainerAndPlaySound(Title title) { + if (TitleContainer.addTitle(title)) { + playNotificationSound(); + } + } + + /** + * Adds the title to {@link TitleContainer} for a set number of ticks and {@link #playNotificationSound() plays the notification sound} if the title is not in the {@link TitleContainer} already. + * No checking needs to be done on whether the title is in the {@link TitleContainer} already by the caller. + * + * @param title the title + * @param ticks the number of ticks the title will remain + */ + public static void displayInTitleContainerAndPlaySound(Title title, int ticks) { + if (TitleContainer.addTitle(title, ticks)) { + playNotificationSound(); + } + } + + private static void playNotificationSound() { + if (MinecraftClient.getInstance().player != null) { + MinecraftClient.getInstance().player.playSound(SoundEvents.ENTITY_EXPERIENCE_ORB_PICKUP, 100f, 0.1f); + } + } + + public static boolean pointIsInArea(double x, double y, double x1, double y1, double x2, double y2) { + return x >= x1 && x <= x2 && y >= y1 && y <= y2; + } +} diff --git a/src/main/java/de/hysky/skyblocker/utils/render/culling/OcclusionCulling.java b/src/main/java/de/hysky/skyblocker/utils/render/culling/OcclusionCulling.java new file mode 100644 index 00000000..5f8d1592 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/render/culling/OcclusionCulling.java @@ -0,0 +1,47 @@ +package de.hysky.skyblocker.utils.render.culling; + +import com.logisticscraft.occlusionculling.OcclusionCullingInstance; +import com.logisticscraft.occlusionculling.cache.ArrayOcclusionCache; +import com.logisticscraft.occlusionculling.util.Vec3d; +import de.hysky.skyblocker.utils.render.FrustumUtils; +import net.minecraft.client.MinecraftClient; + +public class OcclusionCulling { + private static final int TRACING_DISTANCE = 128; + private static final MinecraftClient CLIENT = MinecraftClient.getInstance(); + private static OcclusionCullingInstance instance = null; + + // Reused objects to reduce allocation overhead + private static final Vec3d cameraPos = new Vec3d(0, 0, 0); + private static final Vec3d min = new Vec3d(0, 0, 0); + private static final Vec3d max = new Vec3d(0, 0, 0); + + /** + * Initializes the occlusion culling instance + */ + public static void init() { + instance = new OcclusionCullingInstance(TRACING_DISTANCE, new WorldProvider(), new ArrayOcclusionCache(TRACING_DISTANCE), 2); + } + + private static void updateCameraPos() { + var camera = CLIENT.gameRenderer.getCamera().getPos(); + cameraPos.set(camera.x, camera.y, camera.z); + } + + /** + * This first checks checks if the bounding box is within the camera's FOV, if + * it is then it checks for whether it's occluded or not. + * + * @return A boolean representing whether the bounding box is fully visible or + * not. + */ + public static boolean isVisible(double x1, double y1, double z1, double x2, double y2, double z2) { + if (!FrustumUtils.isVisible(x1, y1, z1, x2, y2, z2)) return false; + + updateCameraPos(); + min.set(x1, y1, z1); + max.set(x2, y2, z2); + + return instance.isAABBVisible(min, max, cameraPos); + } +} diff --git a/src/main/java/de/hysky/skyblocker/utils/render/culling/WorldProvider.java b/src/main/java/de/hysky/skyblocker/utils/render/culling/WorldProvider.java new file mode 100644 index 00000000..7ee0f0ed --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/render/culling/WorldProvider.java @@ -0,0 +1,28 @@ +package de.hysky.skyblocker.utils.render.culling; + +import com.logisticscraft.occlusionculling.DataProvider; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.util.math.BlockPos; + +public class WorldProvider implements DataProvider { + private final static MinecraftClient CLIENT = MinecraftClient.getInstance(); + private ClientWorld world = null; + + @Override + public boolean prepareChunk(int chunkX, int chunkZ) { + this.world = CLIENT.world; + return this.world != null; + } + + @Override + public boolean isOpaqueFullCube(int x, int y, int z) { + BlockPos pos = new BlockPos(x, y, z); + return this.world.getBlockState(pos).isOpaqueFullCube(this.world, pos); + } + + @Override + public void cleanup() { + this.world = null; + } +} diff --git a/src/main/java/de/hysky/skyblocker/utils/render/culling/package-info.java b/src/main/java/de/hysky/skyblocker/utils/render/culling/package-info.java new file mode 100644 index 00000000..1d5cdf98 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/render/culling/package-info.java @@ -0,0 +1,4 @@ +/** + * Package dedicated to occlusion culling utilities + */ +package de.hysky.skyblocker.utils.render.culling;
\ No newline at end of file diff --git a/src/main/java/de/hysky/skyblocker/utils/render/gui/ColorHighlight.java b/src/main/java/de/hysky/skyblocker/utils/render/gui/ColorHighlight.java new file mode 100644 index 00000000..5451e1a8 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/render/gui/ColorHighlight.java @@ -0,0 +1,24 @@ +package de.hysky.skyblocker.utils.render.gui; + +public record ColorHighlight(int slot, int color) { + private static final int RED_HIGHLIGHT = 64 << 24 | 255 << 16; + private static final int YELLOW_HIGHLIGHT = 128 << 24 | 255 << 16 | 255 << 8; + private static final int GREEN_HIGHLIGHT = 128 << 24 | 64 << 16 | 196 << 8 | 64; + private static final int GRAY_HIGHLIGHT = 128 << 24 | 64 << 16 | 64 << 8 | 64; + + public static ColorHighlight red(int slot) { + return new ColorHighlight(slot, RED_HIGHLIGHT); + } + + public static ColorHighlight yellow(int slot) { + return new ColorHighlight(slot, YELLOW_HIGHLIGHT); + } + + public static ColorHighlight green(int slot) { + return new ColorHighlight(slot, GREEN_HIGHLIGHT); + } + + public static ColorHighlight gray(int slot) { + return new ColorHighlight(slot, GRAY_HIGHLIGHT); + } +}
\ No newline at end of file diff --git a/src/main/java/de/hysky/skyblocker/utils/render/gui/ContainerSolver.java b/src/main/java/de/hysky/skyblocker/utils/render/gui/ContainerSolver.java new file mode 100644 index 00000000..cf67e84c --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/render/gui/ContainerSolver.java @@ -0,0 +1,44 @@ +package de.hysky.skyblocker.utils.render.gui; + +import net.minecraft.client.gui.screen.ingame.GenericContainerScreen; +import net.minecraft.item.ItemStack; + +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +/** + * Abstract class for gui solvers. Extend this class to add a new gui solver, like terminal solvers or experiment solvers. + */ +public abstract class ContainerSolver { + private final Pattern containerName; + + protected ContainerSolver(String containerName) { + this.containerName = Pattern.compile(containerName); + } + + protected abstract boolean isEnabled(); + + public Pattern getName() { + return containerName; + } + + protected void start(GenericContainerScreen screen) { + } + + protected void reset() { + } + + protected abstract List<ColorHighlight> getColors(String[] groups, Map<Integer, ItemStack> slots); + + protected void trimEdges(Map<Integer, ItemStack> slots, int rows) { + for (int i = 0; i < rows; i++) { + slots.remove(9 * i); + slots.remove(9 * i + 8); + } + for (int i = 1; i < 8; i++) { + slots.remove(i); + slots.remove((rows - 1) * 9 + i); + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/utils/render/gui/ContainerSolverManager.java b/src/main/java/de/hysky/skyblocker/utils/render/gui/ContainerSolverManager.java new file mode 100644 index 00000000..60e2b4e4 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/render/gui/ContainerSolverManager.java @@ -0,0 +1,125 @@ +package de.hysky.skyblocker.utils.render.gui; + +import com.mojang.blaze3d.systems.RenderSystem; +import de.hysky.skyblocker.mixin.accessor.HandledScreenAccessor; +import de.hysky.skyblocker.skyblock.dungeon.CroesusHelper; +import de.hysky.skyblocker.skyblock.dungeon.terminal.ColorTerminal; +import de.hysky.skyblocker.skyblock.dungeon.terminal.OrderTerminal; +import de.hysky.skyblocker.skyblock.dungeon.terminal.StartsWithTerminal; +import de.hysky.skyblocker.skyblock.experiment.ChronomatronSolver; +import de.hysky.skyblocker.skyblock.experiment.SuperpairsSolver; +import de.hysky.skyblocker.skyblock.experiment.UltrasequencerSolver; +import de.hysky.skyblocker.utils.Utils; +import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.ingame.GenericContainerScreen; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.item.ItemStack; +import net.minecraft.screen.slot.Slot; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Manager class for {@link ContainerSolver}s like terminal solvers and experiment solvers. To add a new gui solver, extend {@link ContainerSolver} and register it in {@link #ContainerSolverManager()}. + */ +public class ContainerSolverManager { + private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile(""); + private final ContainerSolver[] solvers; + private ContainerSolver currentSolver = null; + private String[] groups; + private List<ColorHighlight> highlights; + + public ContainerSolverManager() { + solvers = new ContainerSolver[]{ + new ColorTerminal(), + new OrderTerminal(), + new StartsWithTerminal(), + new CroesusHelper(), + new ChronomatronSolver(), + new SuperpairsSolver(), + new UltrasequencerSolver() + }; + } + + public ContainerSolver getCurrentSolver() { + return currentSolver; + } + + public void init() { + ScreenEvents.BEFORE_INIT.register((client, screen, scaledWidth, scaledHeight) -> { + if (Utils.isOnSkyblock() && screen instanceof GenericContainerScreen genericContainerScreen) { + ScreenEvents.afterRender(screen).register((screen1, context, mouseX, mouseY, delta) -> { + MatrixStack matrices = context.getMatrices(); + matrices.push(); + matrices.translate(((HandledScreenAccessor) genericContainerScreen).getX(), ((HandledScreenAccessor) genericContainerScreen).getY(), 300); + onDraw(context, genericContainerScreen.getScreenHandler().slots.subList(0, genericContainerScreen.getScreenHandler().getRows() * 9)); + matrices.pop(); + }); + ScreenEvents.remove(screen).register(screen1 -> clearScreen()); + onSetScreen(genericContainerScreen); + } else { + clearScreen(); + } + }); + } + + public void onSetScreen(@NotNull GenericContainerScreen screen) { + String screenName = screen.getTitle().getString(); + Matcher matcher = PLACEHOLDER_PATTERN.matcher(screenName); + for (ContainerSolver solver : solvers) { + if (solver.isEnabled()) { + matcher.usePattern(solver.getName()); + matcher.reset(); + if (matcher.matches()) { + currentSolver = solver; + groups = new String[matcher.groupCount()]; + for (int i = 0; i < groups.length; i++) { + groups[i] = matcher.group(i + 1); + } + currentSolver.start(screen); + return; + } + } + } + clearScreen(); + } + + public void clearScreen() { + if (currentSolver != null) { + currentSolver.reset(); + currentSolver = null; + } + } + + public void markDirty() { + highlights = null; + } + + public void onDraw(DrawContext context, List<Slot> slots) { + if (currentSolver == null) + return; + if (highlights == null) + highlights = currentSolver.getColors(groups, slotMap(slots)); + RenderSystem.enableDepthTest(); + RenderSystem.colorMask(true, true, true, false); + for (ColorHighlight highlight : highlights) { + Slot slot = slots.get(highlight.slot()); + int color = highlight.color(); + context.fillGradient(slot.x, slot.y, slot.x + 16, slot.y + 16, color, color); + } + RenderSystem.colorMask(true, true, true, true); + } + + private Map<Integer, ItemStack> slotMap(List<Slot> slots) { + Map<Integer, ItemStack> slotMap = new TreeMap<>(); + for (int i = 0; i < slots.size(); i++) { + slotMap.put(i, slots.get(i).getStack()); + } + return slotMap; + } +} diff --git a/src/main/java/de/hysky/skyblocker/utils/render/title/Title.java b/src/main/java/de/hysky/skyblocker/utils/render/title/Title.java new file mode 100644 index 00000000..1e167afa --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/render/title/Title.java @@ -0,0 +1,53 @@ +package de.hysky.skyblocker.utils.render.title; + +import net.minecraft.text.MutableText; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; + +/** + * Represents a title used for {@link TitleContainer}. + * + * @see TitleContainer + */ +public class Title { + private MutableText text; + protected float x = -1; + protected float y = -1; + + /** + * Constructs a new title with the given translation key and formatting to be applied. + * + * @param textKey the translation key + * @param formatting the formatting to be applied to the text + */ + public Title(String textKey, Formatting formatting) { + this(Text.translatable(textKey).formatted(formatting)); + } + + /** + * Constructs a new title with the given {@link MutableText}. + * Use {@link Text#literal(String)} or {@link Text#translatable(String)} to create a {@link MutableText} + * + * @param text the mutable text + */ + public Title(MutableText text) { + this.text = text; + } + + public MutableText getText() { + return text; + } + + public void setText(MutableText text) { + this.text = text; + } + + protected boolean isDefaultPos() { + return x == -1 && y == -1; + } + + protected void resetPos() { + this.x = -1; + this.y = -1; + } +} diff --git a/src/main/java/de/hysky/skyblocker/utils/render/title/TitleContainer.java b/src/main/java/de/hysky/skyblocker/utils/render/title/TitleContainer.java new file mode 100644 index 00000000..487e3d8b --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/render/title/TitleContainer.java @@ -0,0 +1,175 @@ +package de.hysky.skyblocker.utils.render.title; + +import de.hysky.skyblocker.config.SkyblockerConfig; +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.utils.scheduler.Scheduler; +import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager; +import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; +import net.fabricmc.fabric.api.client.rendering.v1.HudRenderCallback; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.util.math.MathHelper; + +import java.util.LinkedHashSet; +import java.util.Set; + +public class TitleContainer { + /** + * The set of titles which will be rendered. + * + * @see #containsTitle(Title) + * @see #addTitle(Title) + * @see #addTitle(Title, int) + * @see #removeTitle(Title) + */ + private static final Set<Title> titles = new LinkedHashSet<>(); + + public static void init() { + HudRenderCallback.EVENT.register(TitleContainer::render); + ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> dispatcher.register(ClientCommandManager.literal("skyblocker") + .then(ClientCommandManager.literal("hud") + .then(ClientCommandManager.literal("titleContainer") + .executes(Scheduler.queueOpenScreenCommand(TitleContainerConfigScreen::new)))))); + } + + /** + * Returns {@code true} if the title is currently shown. + * + * @param title the title to check + * @return whether the title in currently shown + */ + public static boolean containsTitle(Title title) { + return titles.contains(title); + } + + /** + * Adds a title to be shown + * + * @param title the title to be shown + * @return whether the title is already currently being shown + */ + public static boolean addTitle(Title title) { + if (titles.add(title)) { + title.resetPos(); + return true; + } + return false; + } + + /** + * Adds a title to be shown for a set number of ticks + * + * @param title the title to be shown + * @param ticks the number of ticks to show the title + * @return whether the title is already currently being shown + */ + public static boolean addTitle(Title title, int ticks) { + if (addTitle(title)) { + Scheduler.INSTANCE.schedule(() -> TitleContainer.removeTitle(title), ticks); + return true; + } + return false; + } + + /** + * Stops showing a title + * + * @param title the title to stop showing + */ + public static void removeTitle(Title title) { + titles.remove(title); + } + + private static void render(DrawContext context, float tickDelta) { + render(context, titles, SkyblockerConfigManager.get().general.titleContainer.x, SkyblockerConfigManager.get().general.titleContainer.y, tickDelta); + } + + protected static void render(DrawContext context, Set<Title> titles, int xPos, int yPos, float tickDelta) { + var client = MinecraftClient.getInstance(); + TextRenderer textRenderer = client.textRenderer; + + // Calculate Scale to use + float scale = 3F * (SkyblockerConfigManager.get().general.titleContainer.titleContainerScale / 100F); + + // Grab direction and alignment values + SkyblockerConfig.Direction direction = SkyblockerConfigManager.get().general.titleContainer.direction; + SkyblockerConfig.Alignment alignment = SkyblockerConfigManager.get().general.titleContainer.alignment; + // x/y refer to the starting position for the text + // y always starts at yPos + float x = 0; + float y = yPos; + + //Calculate the width of combined text + float width = 0; + for (Title title : titles) { + width += textRenderer.getWidth(title.getText()) * scale + 10; + } + + if (alignment == SkyblockerConfig.Alignment.MIDDLE) { + if (direction == SkyblockerConfig.Direction.HORIZONTAL) { + //If middle aligned horizontally, start the xPosition at half of the width to the left. + x = xPos - (width / 2); + } else { + //If middle aligned vertically, start at xPos, we will shift each text to the left later + x = xPos; + } + } + if (alignment == SkyblockerConfig.Alignment.LEFT || alignment == SkyblockerConfig.Alignment.RIGHT) { + //If left or right aligned, start at xPos, we will shift each text later + x = xPos; + } + + for (Title title : titles) { + + //Calculate which x the text should use + float xToUse; + if (direction == SkyblockerConfig.Direction.HORIZONTAL) { + xToUse = alignment == SkyblockerConfig.Alignment.RIGHT ? + x - (textRenderer.getWidth(title.getText()) * scale) : //if right aligned we need the text position to be aligned on the right side. + x; + } else { + xToUse = alignment == SkyblockerConfig.Alignment.MIDDLE ? + x - (textRenderer.getWidth(title.getText()) * scale) / 2 : //if middle aligned we need the text position to be aligned in the middle. + alignment == SkyblockerConfig.Alignment.RIGHT ? + x - (textRenderer.getWidth(title.getText()) * scale) : //if right aligned we need the text position to be aligned on the right side. + x; + } + + //Start displaying the title at the correct position, not at the default position + if (title.isDefaultPos()) { + title.x = xToUse; + title.y = y; + } + + //Lerp the texts x and y variables + title.x = MathHelper.lerp(tickDelta * 0.5F, title.x, xToUse); + title.y = MathHelper.lerp(tickDelta * 0.5F, title.y, y); + + //Translate the matrix to the texts position and scale + context.getMatrices().push(); + context.getMatrices().translate(title.x, title.y, 200); + context.getMatrices().scale(scale, scale, scale); + + //Draw text + context.drawTextWithShadow(textRenderer, title.getText(), 0, 0, 0xFFFFFF); + context.getMatrices().pop(); + + //Calculate the x and y positions for the next title + if (direction == SkyblockerConfig.Direction.HORIZONTAL) { + if (alignment == SkyblockerConfig.Alignment.MIDDLE || alignment == SkyblockerConfig.Alignment.LEFT) { + //Move to the right if middle or left aligned + x += textRenderer.getWidth(title.getText()) * scale + 10; + } + + if (alignment == SkyblockerConfig.Alignment.RIGHT) { + //Move to the left if right aligned + x -= textRenderer.getWidth(title.getText()) * scale + 10; + } + } else { + //Y always moves by the same amount if vertical + y += textRenderer.fontHeight * scale + 10; + } + } + } +}
\ No newline at end of file diff --git a/src/main/java/de/hysky/skyblocker/utils/render/title/TitleContainerConfigScreen.java b/src/main/java/de/hysky/skyblocker/utils/render/title/TitleContainerConfigScreen.java new file mode 100644 index 00000000..5a42eeb4 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/render/title/TitleContainerConfigScreen.java @@ -0,0 +1,170 @@ +package de.hysky.skyblocker.utils.render.title; + +import de.hysky.skyblocker.config.SkyblockerConfig; +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.utils.render.RenderHelper; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.util.math.Vector2f; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import net.minecraft.util.Pair; +import org.lwjgl.glfw.GLFW; + +import java.awt.*; +import java.util.Set; + +public class TitleContainerConfigScreen extends Screen { + private final Title example1 = new Title(Text.literal("Test1").formatted(Formatting.RED)); + private final Title example2 = new Title(Text.literal("Test23").formatted(Formatting.AQUA)); + private final Title example3 = new Title(Text.literal("Testing1234").formatted(Formatting.DARK_GREEN)); + private float hudX = SkyblockerConfigManager.get().general.titleContainer.x; + private float hudY = SkyblockerConfigManager.get().general.titleContainer.y; + private final Screen parent; + + protected TitleContainerConfigScreen() { + this(null); + } + + public TitleContainerConfigScreen(Screen parent) { + super(Text.of("Title Container HUD Config")); + this.parent = parent; + } + + @Override + public void render(DrawContext context, int mouseX, int mouseY, float delta) { + super.render(context, mouseX, mouseY, delta); + renderBackground(context, mouseX, mouseY, delta); + TitleContainer.render(context, Set.of(example1, example2, example3), (int) hudX, (int) hudY, delta); + SkyblockerConfig.Direction direction = SkyblockerConfigManager.get().general.titleContainer.direction; + SkyblockerConfig.Alignment alignment = SkyblockerConfigManager.get().general.titleContainer.alignment; + context.drawCenteredTextWithShadow(textRenderer, "Press Q/E to change Alignment: " + alignment, width / 2, textRenderer.fontHeight * 2, Color.WHITE.getRGB()); + context.drawCenteredTextWithShadow(textRenderer, "Press R to change Direction: " + direction, width / 2, textRenderer.fontHeight * 3 + 5, Color.WHITE.getRGB()); + context.drawCenteredTextWithShadow(textRenderer, "Press +/- to change Scale", width / 2, textRenderer.fontHeight * 4 + 10, Color.WHITE.getRGB()); + context.drawCenteredTextWithShadow(textRenderer, "Right Click To Reset Position", width / 2, textRenderer.fontHeight * 5 + 15, Color.GRAY.getRGB()); + + Pair<Vector2f, Vector2f> boundingBox = getSelectionBoundingBox(); + int x1 = (int) boundingBox.getLeft().getX(); + int y1 = (int) boundingBox.getLeft().getY(); + int x2 = (int) boundingBox.getRight().getX(); + int y2 = (int) boundingBox.getRight().getY(); + + context.drawHorizontalLine(x1, x2, y1, Color.RED.getRGB()); + context.drawHorizontalLine(x1, x2, y2, Color.RED.getRGB()); + context.drawVerticalLine(x1, y1, y2, Color.RED.getRGB()); + context.drawVerticalLine(x2, y1, y2, Color.RED.getRGB()); + } + + private Pair<Vector2f, Vector2f> getSelectionBoundingBox() { + SkyblockerConfig.Alignment alignment = SkyblockerConfigManager.get().general.titleContainer.alignment; + + float midWidth = getSelectionWidth() / 2F; + float x1 = 0; + float x2 = 0; + float y1 = hudY; + float y2 = hudY + getSelectionHeight(); + switch (alignment) { + case RIGHT -> { + x1 = hudX - midWidth * 2; + x2 = hudX; + } + case MIDDLE -> { + x1 = hudX - midWidth; + x2 = hudX + midWidth; + } + case LEFT -> { + x1 = hudX; + x2 = hudX + midWidth * 2; + } + } + return new Pair<>(new Vector2f(x1, y1), new Vector2f(x2, y2)); + } + + private float getSelectionHeight() { + float scale = (3F * (SkyblockerConfigManager.get().general.titleContainer.titleContainerScale / 100F)); + return SkyblockerConfigManager.get().general.titleContainer.direction == SkyblockerConfig.Direction.HORIZONTAL ? + (textRenderer.fontHeight * scale) : + (textRenderer.fontHeight + 10F) * 3F * scale; + } + + private float getSelectionWidth() { + float scale = (3F * (SkyblockerConfigManager.get().general.titleContainer.titleContainerScale / 100F)); + return SkyblockerConfigManager.get().general.titleContainer.direction == SkyblockerConfig.Direction.HORIZONTAL ? + (textRenderer.getWidth("Test1") + 10 + textRenderer.getWidth("Test23") + 10 + textRenderer.getWidth("Testing1234")) * scale : + textRenderer.getWidth("Testing1234") * scale; + } + + @Override + public boolean mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) { + float midWidth = getSelectionWidth() / 2; + float midHeight = getSelectionHeight() / 2; + var alignment = SkyblockerConfigManager.get().general.titleContainer.alignment; + + Pair<Vector2f, Vector2f> boundingBox = getSelectionBoundingBox(); + float x1 = boundingBox.getLeft().getX(); + float y1 = boundingBox.getLeft().getY(); + float x2 = boundingBox.getRight().getX(); + float y2 = boundingBox.getRight().getY(); + + if (RenderHelper.pointIsInArea(mouseX, mouseY, x1, y1, x2, y2) && button == 0) { + hudX = switch (alignment) { + case LEFT -> (int) mouseX - midWidth; + case MIDDLE -> (int) mouseX; + case RIGHT -> (int) mouseX + midWidth; + }; + hudY = (int) (mouseY - midHeight); + } + return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (button == 1) { + hudX = (float) this.width / 2; + hudY = this.height * 0.6F; + } + return super.mouseClicked(mouseX, mouseY, button); + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (keyCode == GLFW.GLFW_KEY_Q) { + SkyblockerConfig.Alignment current = SkyblockerConfigManager.get().general.titleContainer.alignment; + SkyblockerConfigManager.get().general.titleContainer.alignment = switch (current) { + case LEFT -> SkyblockerConfig.Alignment.MIDDLE; + case MIDDLE -> SkyblockerConfig.Alignment.RIGHT; + case RIGHT -> SkyblockerConfig.Alignment.LEFT; + }; + } + if (keyCode == GLFW.GLFW_KEY_E) { + SkyblockerConfig.Alignment current = SkyblockerConfigManager.get().general.titleContainer.alignment; + SkyblockerConfigManager.get().general.titleContainer.alignment = switch (current) { + case LEFT -> SkyblockerConfig.Alignment.RIGHT; + case MIDDLE -> SkyblockerConfig.Alignment.LEFT; + case RIGHT -> SkyblockerConfig.Alignment.MIDDLE; + }; + } + if (keyCode == GLFW.GLFW_KEY_R) { + SkyblockerConfig.Direction current = SkyblockerConfigManager.get().general.titleContainer.direction; + SkyblockerConfigManager.get().general.titleContainer.direction = switch (current) { + case HORIZONTAL -> SkyblockerConfig.Direction.VERTICAL; + case VERTICAL -> SkyblockerConfig.Direction.HORIZONTAL; + }; + } + if (keyCode == GLFW.GLFW_KEY_EQUAL) { + SkyblockerConfigManager.get().general.titleContainer.titleContainerScale += 10; + } + if (keyCode == GLFW.GLFW_KEY_MINUS) { + SkyblockerConfigManager.get().general.titleContainer.titleContainerScale -= 10; + } + return super.keyPressed(keyCode, scanCode, modifiers); + } + + @Override + public void close() { + SkyblockerConfigManager.get().general.titleContainer.x = (int) hudX; + SkyblockerConfigManager.get().general.titleContainer.y = (int) hudY; + SkyblockerConfigManager.save(); + this.client.setScreen(parent); + } +} diff --git a/src/main/java/de/hysky/skyblocker/utils/scheduler/MessageScheduler.java b/src/main/java/de/hysky/skyblocker/utils/scheduler/MessageScheduler.java new file mode 100644 index 00000000..15636965 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/scheduler/MessageScheduler.java @@ -0,0 +1,66 @@ +package de.hysky.skyblocker.utils.scheduler; + +import net.minecraft.client.MinecraftClient; + +/** + * A scheduler for sending chat messages or commands. Use the instance in {@link #INSTANCE}. Do not instantiate this class. + */ +public class MessageScheduler extends Scheduler { + /** + * The minimum delay that the server will accept between chat messages. + */ + private static final int MIN_DELAY = 200; + public static final MessageScheduler INSTANCE = new MessageScheduler(); + /** + * The timestamp of the last message send, + */ + private long lastMessage = 0; + + protected MessageScheduler() { + } + + /** + * Sends a chat message or command after the minimum cooldown. Prefer this method to send messages or commands to the server. + * + * @param message the message to send + */ + public void sendMessageAfterCooldown(String message) { + if (lastMessage + MIN_DELAY < System.currentTimeMillis()) { + sendMessage(message); + lastMessage = System.currentTimeMillis(); + } else { + queueMessage(message, 0); + } + } + + private void sendMessage(String message) { + if (MinecraftClient.getInstance().player != null) { + if (message.startsWith("/")) { + MinecraftClient.getInstance().player.networkHandler.sendCommand(message.substring(1)); + } else { + MinecraftClient.getInstance().inGameHud.getChatHud().addToMessageHistory(message); + MinecraftClient.getInstance().player.networkHandler.sendChatMessage(message); + } + } + } + + /** + * Queues a chat message or command to send in {@code delay} ticks. Use this method to send messages or commands a set time in the future. The minimum cooldown is still respected. + * + * @param message the message to send + * @param delay the delay before sending the message in ticks + */ + public void queueMessage(String message, int delay) { + schedule(() -> sendMessage(message), delay); + } + + @Override + protected boolean runTask(Runnable task) { + if (lastMessage + MIN_DELAY < System.currentTimeMillis()) { + task.run(); + lastMessage = System.currentTimeMillis(); + return true; + } + return false; + } +} diff --git a/src/main/java/de/hysky/skyblocker/utils/scheduler/Scheduler.java b/src/main/java/de/hysky/skyblocker/utils/scheduler/Scheduler.java new file mode 100644 index 00000000..0f44cf93 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/scheduler/Scheduler.java @@ -0,0 +1,140 @@ +package de.hysky.skyblocker.utils.scheduler; + +import com.mojang.brigadier.Command; +import it.unimi.dsi.fastutil.ints.AbstractInt2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.screen.Screen; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +/** + * A scheduler for running tasks at a later time. Tasks will be run synchronously on the main client thread. Use the instance stored in {@link #INSTANCE}. Do not instantiate this class. + */ +public class Scheduler { + private static final Logger LOGGER = LoggerFactory.getLogger(Scheduler.class); + public static final Scheduler INSTANCE = new Scheduler(); + private int currentTick = 0; + private final AbstractInt2ObjectMap<List<ScheduledTask>> tasks = new Int2ObjectOpenHashMap<>(); + + protected Scheduler() { + } + + /** + * Schedules a task to run after a delay. + * + * @param task the task to run + * @param delay the delay in ticks + */ + public void schedule(Runnable task, int delay) { + if (delay >= 0) { + addTask(new ScheduledTask(task), currentTick + delay); + } else { + LOGGER.warn("Scheduled a task with negative delay"); + } + } + + /** + * Schedules a task to run every period ticks. + * + * @param task the task to run + * @param period the period in ticks + */ + public void scheduleCyclic(Runnable task, int period) { + if (period > 0) { + addTask(new CyclicTask(task, period), currentTick); + } else { + LOGGER.error("Attempted to schedule a cyclic task with period lower than 1"); + } + } + + public static Command<FabricClientCommandSource> queueOpenScreenCommand(Supplier<Screen> screenSupplier) { + return context -> INSTANCE.queueOpenScreen(screenSupplier); + } + + /** + * Schedules a screen to open in the next tick. Used in commands to avoid screen immediately closing after the command is executed. + * + * @param screenSupplier the supplier of the screen to open + * @see #queueOpenScreenCommand(Supplier) + */ + public int queueOpenScreen(Supplier<Screen> screenSupplier) { + MinecraftClient.getInstance().send(() -> MinecraftClient.getInstance().setScreen(screenSupplier.get())); + return Command.SINGLE_SUCCESS; + } + + public void tick() { + if (tasks.containsKey(currentTick)) { + List<ScheduledTask> currentTickTasks = tasks.get(currentTick); + //noinspection ForLoopReplaceableByForEach (or else we get a ConcurrentModificationException) + for (int i = 0; i < currentTickTasks.size(); i++) { + ScheduledTask task = currentTickTasks.get(i); + if (!runTask(task)) { + tasks.computeIfAbsent(currentTick + 1, key -> new ArrayList<>()).add(task); + } + } + tasks.remove(currentTick); + } + currentTick += 1; + } + + /** + * Runs the task if able. + * + * @param task the task to run + * @return {@code true} if the task is run, and {@link false} if task is not run. + */ + protected boolean runTask(Runnable task) { + task.run(); + return true; + } + + private void addTask(ScheduledTask scheduledTask, int schedule) { + if (tasks.containsKey(schedule)) { + tasks.get(schedule).add(scheduledTask); + } else { + List<ScheduledTask> list = new ArrayList<>(); + list.add(scheduledTask); + tasks.put(schedule, list); + } + } + + /** + * A task that runs every period ticks. More specifically, this task reschedules itself to run again after period ticks every time it runs. + */ + protected class CyclicTask extends ScheduledTask { + private final int period; + + CyclicTask(Runnable inner, int period) { + super(inner); + this.period = period; + } + + @Override + public void run() { + super.run(); + addTask(this, currentTick + period); + } + } + + /** + * A task that runs at a specific tick, relative to {@link #currentTick}. + */ + protected static class ScheduledTask implements Runnable { + private final Runnable inner; + + public ScheduledTask(Runnable inner) { + this.inner = inner; + } + + @Override + public void run() { + inner.run(); + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/utils/tictactoe/TicTacToeUtils.java b/src/main/java/de/hysky/skyblocker/utils/tictactoe/TicTacToeUtils.java new file mode 100644 index 00000000..908ba46d --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/tictactoe/TicTacToeUtils.java @@ -0,0 +1,104 @@ +package de.hysky.skyblocker.utils.tictactoe; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public class TicTacToeUtils { + + public static int getBestMove(char[][] board) { + HashMap<Integer, Integer> moves = new HashMap<>(); + for (int row = 0; row < board.length; row++) { + for (int col = 0; col < board[row].length; col++) { + if (board[row][col] != '\0') continue; + board[row][col] = 'O'; + int score = alphabeta(board, Integer.MIN_VALUE, Integer.MAX_VALUE, false, 0); + board[row][col] = '\0'; + moves.put(row * 3 + col + 1, score); + } + } + return Collections.max(moves.entrySet(), Map.Entry.comparingByValue()).getKey(); + } + + public static boolean hasMovesLeft(char[][] board) { + for (char[] rows : board) { + for (char col : rows) { + if (col == '\0') return true; + } + } + return false; + } + + public static int getBoardRanking(char[][] board) { + for (int row = 0; row < 3; row++) { + if (board[row][0] == board[row][1] && board[row][0] == board[row][2]) { + if (board[row][0] == 'X') { + return -10; + } else if (board[row][0] == 'O') { + return 10; + } + } + } + + for (int col = 0; col < 3; col++) { + if (board[0][col] == board[1][col] && board[0][col] == board[2][col]) { + if (board[0][col] == 'X') { + return -10; + } else if (board[0][col] == 'O') { + return 10; + } + } + } + + if (board[0][0] == board[1][1] && board[0][0] == board[2][2]) { + if (board[0][0] == 'X') { + return -10; + } else if (board[0][0] == 'O') { + return 10; + } + } else if (board[0][2] == board[1][1] && board[0][2] == board[2][0]) { + if (board[0][2] == 'X') { + return -10; + } else if (board[0][2] == 'O') { + return 10; + } + } + + return 0; + } + public static int alphabeta(char[][] board, int alpha, int beta, boolean max, int depth) { + int score = getBoardRanking(board); + if (score == 10 || score == -10) return score; + if (!hasMovesLeft(board)) return 0; + + if (max) { + int bestScore = Integer.MIN_VALUE; + for (int row = 0; row < 3; row++) { + for (int col = 0; col < 3; col++) { + if (board[row][col] == '\0') { + board[row][col] = 'O'; + bestScore = Math.max(bestScore, alphabeta(board, alpha, beta, false, depth + 1)); + board[row][col] = '\0'; + alpha = Math.max(alpha, bestScore); + if (beta <= alpha) break; // Pruning + } + } + } + return bestScore - depth; + } else { + int bestScore = Integer.MAX_VALUE; + for (int row = 0; row < 3; row++) { + for (int col = 0; col < 3; col++) { + if (board[row][col] == '\0') { + board[row][col] = 'X'; + bestScore = Math.min(bestScore, alphabeta(board, alpha, beta, true, depth + 1)); + board[row][col] = '\0'; + beta = Math.min(beta, bestScore); + if (beta <= alpha) break; // Pruning + } + } + } + return bestScore + depth; + } + } +}
\ No newline at end of file |