From cdd9441aa587ef7ecb2ac285350f59846b433ff9 Mon Sep 17 00:00:00 2001 From: Emirlol <81419447+Emirlol@users.noreply.github.com> Date: Sun, 7 Jan 2024 02:06:28 +0300 Subject: Add dungeon score calculation on client-side --- .../hysky/skyblocker/config/SkyblockerConfig.java | 14 + .../config/categories/DungeonsCategory.java | 30 ++- .../config/categories/MessageFilterCategory.java | 8 + .../skyblocker/skyblock/dungeon/DungeonScore.java | 290 ++++++++++++++++++--- .../skyblocker/skyblock/filters/MimicFilter.java | 26 ++ src/main/java/de/hysky/skyblocker/utils/Utils.java | 27 ++ .../skyblocker/utils/chat/ChatMessageListener.java | 7 +- .../resources/assets/skyblocker/lang/en_us.json | 7 + 8 files changed, 363 insertions(+), 46 deletions(-) create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/filters/MimicFilter.java diff --git a/src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java b/src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java index aa6e5d24..e07a7588 100644 --- a/src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java +++ b/src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java @@ -605,6 +605,9 @@ public class SkyblockerConfig { @SerialEntry public DungeonChestProfit dungeonChestProfit = new DungeonChestProfit(); + @SerialEntry + public MimicMessages mimicMessages = new MimicMessages(); + @SerialEntry public boolean croesusHelper = true; @@ -785,6 +788,14 @@ public class SkyblockerConfig { public Formatting incompleteColor = Formatting.BLUE; } + public static class MimicMessages { + @SerialEntry + public boolean sendMimicMessages = true; + + @SerialEntry + public String mimicMessage = "Mimic dead!"; + } + public static class LividColor { @SerialEntry public boolean enableLividColorGlow = true; @@ -969,6 +980,9 @@ public class SkyblockerConfig { @SerialEntry public ChatFilterResult hideToggleSkyMall = ChatFilterResult.PASS; + @SerialEntry + public ChatFilterResult hideMimicKill = ChatFilterResult.PASS; + @SerialEntry public boolean hideMana = false; } diff --git a/src/main/java/de/hysky/skyblocker/config/categories/DungeonsCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/DungeonsCategory.java index 583bc166..06133afc 100644 --- a/src/main/java/de/hysky/skyblocker/config/categories/DungeonsCategory.java +++ b/src/main/java/de/hysky/skyblocker/config/categories/DungeonsCategory.java @@ -2,17 +2,12 @@ package de.hysky.skyblocker.config.categories; import de.hysky.skyblocker.config.ConfigUtils; import de.hysky.skyblocker.config.SkyblockerConfig; +import de.hysky.skyblocker.skyblock.dungeon.DungeonMapConfigScreen; import de.hysky.skyblocker.utils.waypoint.Waypoint.Type; -import dev.isxander.yacl3.api.ButtonOption; -import dev.isxander.yacl3.api.ConfigCategory; -import dev.isxander.yacl3.api.Option; -import dev.isxander.yacl3.api.OptionDescription; -import dev.isxander.yacl3.api.OptionFlag; -import dev.isxander.yacl3.api.OptionGroup; +import dev.isxander.yacl3.api.*; import dev.isxander.yacl3.api.controller.FloatFieldControllerBuilder; import dev.isxander.yacl3.api.controller.IntegerFieldControllerBuilder; import dev.isxander.yacl3.api.controller.StringControllerBuilder; -import de.hysky.skyblocker.skyblock.dungeon.DungeonMapConfigScreen; import net.minecraft.client.MinecraftClient; import net.minecraft.text.Text; import net.minecraft.util.Formatting; @@ -418,6 +413,27 @@ public class DungeonsCategory { newValue -> config.locations.dungeons.allowDroppingProtectedItems = newValue) .controller(ConfigUtils::createBooleanController) .build()) + //Mimic Messages + .group(OptionGroup.createBuilder() + .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.mimicMessages")) + .collapsed(true) + .option(Option.createBuilder() + .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.mimicMessages.sendMimicMessages")) + .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.mimicMessages.sendMimicMessages.@Tooltip"))) + .binding(defaults.locations.dungeons.mimicMessages.sendMimicMessages, + () -> config.locations.dungeons.mimicMessages.sendMimicMessages, + newValue -> config.locations.dungeons.mimicMessages.sendMimicMessages = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) + .option(Option.createBuilder() + .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.mimicMessages.mimicMessage")) + .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.mimicMessages.mimicMessage.@Tooltip"))) + .binding(defaults.locations.dungeons.mimicMessages.mimicMessage, + () -> config.locations.dungeons.mimicMessages.mimicMessage, + newValue -> config.locations.dungeons.mimicMessages.mimicMessage = newValue) + .controller(StringControllerBuilder::create) + .build()) + .build()) // Livid Color .group(OptionGroup.createBuilder() diff --git a/src/main/java/de/hysky/skyblocker/config/categories/MessageFilterCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/MessageFilterCategory.java index 37f24d8c..3fe285de 100644 --- a/src/main/java/de/hysky/skyblocker/config/categories/MessageFilterCategory.java +++ b/src/main/java/de/hysky/skyblocker/config/categories/MessageFilterCategory.java @@ -102,6 +102,14 @@ public class MessageFilterCategory { newValue -> config.messages.hideMana = newValue) .controller(ConfigUtils::createBooleanController) .build()) + .option(Option.createBuilder() + .name(Text.translatable("text.autoconfig.skyblocker.option.messages.hideMimicKill")) + .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.messages.hideMimicKill.@Tooltip"))) + .binding(defaults.messages.hideMimicKill, + () -> config.messages.hideMimicKill, + newValue -> config.messages.hideMimicKill = newValue) + .controller(ConfigUtils::createEnumCyclingListController) + .build()) .build(); } } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonScore.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonScore.java index d67d6988..ebec99dc 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonScore.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonScore.java @@ -1,28 +1,81 @@ package de.hysky.skyblocker.skyblock.dungeon; + import de.hysky.skyblocker.config.SkyblockerConfig; import de.hysky.skyblocker.config.SkyblockerConfigManager; -import de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonManager; +import de.hysky.skyblocker.skyblock.tabhud.util.PlayerListMgr; import de.hysky.skyblocker.utils.Constants; import de.hysky.skyblocker.utils.Utils; import de.hysky.skyblocker.utils.scheduler.MessageScheduler; import de.hysky.skyblocker.utils.scheduler.Scheduler; import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; +import net.fabricmc.fabric.api.entity.event.v1.ServerLivingEntityEvents; import net.minecraft.client.MinecraftClient; +import net.minecraft.entity.Entity; +import net.minecraft.entity.mob.ZombieEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.nbt.NbtElement; import net.minecraft.sound.SoundEvents; +import net.minecraft.util.collection.DefaultedList; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; public class DungeonScore { - private static final SkyblockerConfig.DungeonScore CONFIG = SkyblockerConfigManager.get().locations.dungeons.dungeonScore; - private static final Pattern DUNGEON_CLEARED_PATTERN = Pattern.compile("Cleared: (?\\d+)% \\((?\\d+)\\)"); + private static final SkyblockerConfig.DungeonScore SCORE_CONFIG = SkyblockerConfigManager.get().locations.dungeons.dungeonScore; + private static final SkyblockerConfig.MimicMessages MIMIC_MESSAGES_CONFIG = SkyblockerConfigManager.get().locations.dungeons.mimicMessages; + private static final Logger LOGGER = LoggerFactory.getLogger("Skyblocker Dungeon Score"); + private static final Pattern CLEARED_PATTERN = Pattern.compile("Cleared: (?\\d+)%.*"); + private static final Pattern SECRETS_PATTERN = Pattern.compile("Secrets Found: (?\\d+\\.?\\d*)%"); + private static final Pattern PUZZLES_PATTERN = Pattern.compile(".+?(?=:): \\[(?.)](?: \\(\\w+\\))?"); + private static final Pattern PUZZLE_COUNT_PATTERN = Pattern.compile("Puzzles: \\((?\\d+)\\)"); + private static final Pattern TIME_PATTERN = Pattern.compile("Time: (?:(?\\d+(?=h))?h? ?(?\\d+(?=m))?m? ?(?\\d+(?=s))s|Soon!)"); + private static final Pattern CRYPTS_PATTERN = Pattern.compile("Crypts: (?\\d+)"); + private static final Pattern COMPLETED_ROOMS_PATTERN = Pattern.compile(" *Completed Rooms: (?\\d+)"); + private static final Pattern DUNGEON_START_PATTERN = Pattern.compile("(?:Auto-closing|Starting) in: \\d:\\d+"); + private static final Pattern FLOOR_PATTERN = Pattern.compile(".*?(?=T)The Catacombs \\((?[EFM]\\D*\\d*)\\)"); + private static final Pattern DEATHS_PATTERN = Pattern.compile("Team Deaths: (?\\d+)"); + private static String currentFloor; private static boolean sent270; private static boolean sent300; + private static boolean isMimicKilled; + private static int puzzleCount; + //Caching the dungeon start state to prevent unnecessary scoreboard pattern matching after dungeon starts + private static boolean isDungeonStarted; + private static boolean isMayorPaul; + private static final HashMap floorRequirements = new HashMap<>(Map.ofEntries( + Map.entry("E", new FloorRequirement(30, 1200)), + Map.entry("F1", new FloorRequirement(30, 600)), + Map.entry("F2", new FloorRequirement(40, 600)), + Map.entry("F3", new FloorRequirement(50, 600)), + Map.entry("F4", new FloorRequirement(60, 720)), + Map.entry("F5", new FloorRequirement(70, 600)), + Map.entry("F6", new FloorRequirement(85, 720)), + Map.entry("F7", new FloorRequirement(100, 840)), + Map.entry("M1", new FloorRequirement(100, 480)), + Map.entry("M2", new FloorRequirement(100, 480)), + Map.entry("M3", new FloorRequirement(100, 480)), + Map.entry("M4", new FloorRequirement(100, 480)), + Map.entry("M5", new FloorRequirement(100, 480)), + Map.entry("M6", new FloorRequirement(100, 600)), + Map.entry("M7", new FloorRequirement(100, 840)) + )); public static void init() { Scheduler.INSTANCE.scheduleCyclic(DungeonScore::tick, 20); ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> reset()); + ServerLivingEntityEvents.AFTER_DEATH.register((entity, source) -> { + if (isEntityMimic(entity)) { + if (MIMIC_MESSAGES_CONFIG.sendMimicMessages) MessageScheduler.INSTANCE.sendMessageAfterCooldown(MIMIC_MESSAGES_CONFIG.mimicMessage); + setMimicKilled(true); + } + }); } public static void tick() { @@ -31,46 +84,211 @@ public class DungeonScore { reset(); return; } + if (!isDungeonStarted) { + if (checkIfDungeonStarted()) onDungeonStart(); + return; + } + int score = calculateScore(); + if (SCORE_CONFIG.enableDungeonScore270 && !sent270 && score >= 270 && score < 300) { + MessageScheduler.INSTANCE.sendMessageAfterCooldown("/pc " + Constants.PREFIX.get().getString() + SCORE_CONFIG.dungeonScore270Message.replaceAll("\\[score]", "270")); + client.player.playSound(SoundEvents.BLOCK_NOTE_BLOCK_PLING.value(), 1f, 0.1f); + sent270 = true; + } + if (SCORE_CONFIG.enableDungeonScore300 && !sent300 && score >= 300) { + MessageScheduler.INSTANCE.sendMessageAfterCooldown("/pc " + Constants.PREFIX.get().getString() + SCORE_CONFIG.dungeonScore300Message.replaceAll("\\[score]", "300")); + client.player.playSound(SoundEvents.BLOCK_NOTE_BLOCK_PLING.value(), 1f, 1f); + sent300 = true; + } + } + + public static boolean isEntityMimic(Entity entity) { + if (!(entity instanceof ZombieEntity zombie)) return false; + if (!zombie.isBaby()) return false; + try { + DefaultedList armor = (DefaultedList) zombie.getArmorItems(); + if (armor.isEmpty()) return false; + NbtCompound helmetNbt = armor.get(3).getNbt(); + if (helmetNbt == null) return false; + return helmetNbt.getCompound("SkullOwner") + .getCompound("Properties") + .getList("textures", NbtElement.COMPOUND_TYPE) + .getCompound(0).getString("Value") + .equals("eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZTE5YzEyNTQzYmM3NzkyNjA1ZWY2OGUxZjg3NDlhZThmMmEzODFkOTA4NWQ0ZDRiNzgwYmExMjgyZDM1OTdhMCJ9fX0K"); + } catch (NullPointerException e) { + return false; + } catch (ClassCastException f) { + f.printStackTrace(); + return false; + } + } + private static boolean checkIfDungeonStarted() { for (String sidebarLine : Utils.STRING_SCOREBOARD) { - Matcher dungeonClearedMatcher = DUNGEON_CLEARED_PATTERN.matcher(sidebarLine); - if (!dungeonClearedMatcher.matches()) { - continue; - } - int score = Integer.parseInt(dungeonClearedMatcher.group("score")); - if (!DungeonManager.isInBoss()) score += 28; - if (!sent270 && score >= 270 && score < 300) { - if (CONFIG.enableDungeonScore270Message) { - MessageScheduler.INSTANCE.sendMessageAfterCooldown(Constants.PREFIX.get().getString() + CONFIG.dungeonScore270Message.replaceAll("\\[score]", "270")); - } - if (CONFIG.enableDungeonScore270Title) { - client.inGameHud.setDefaultTitleFade(); - client.inGameHud.setTitle(Constants.PREFIX.get().append(CONFIG.dungeonScore270Message.replaceAll("\\[score]", "270"))); - } - if (CONFIG.enableDungeonScore270Sound) { - client.player.playSound(SoundEvents.BLOCK_NOTE_BLOCK_PLING.value(), 100f, 0.1f); - } - sent270 = true; - } - if (!sent300 && score >= 300) { - if (CONFIG.enableDungeonScore300Message) { - MessageScheduler.INSTANCE.sendMessageAfterCooldown(Constants.PREFIX.get().getString() + CONFIG.dungeonScore300Message.replaceAll("\\[score]", "300")); - } - if (CONFIG.enableDungeonScore300Title) { - client.inGameHud.setDefaultTitleFade(); - client.inGameHud.setTitle(Constants.PREFIX.get().append(CONFIG.dungeonScore300Message.replaceAll("\\[score]", "300"))); - } - if (CONFIG.enableDungeonScore300Sound) { - client.player.playSound(SoundEvents.BLOCK_NOTE_BLOCK_PLING.value(), 100f, 0.1f); - } - sent300 = true; - } - break; + Matcher matcher = DUNGEON_START_PATTERN.matcher(sidebarLine); + if (matcher.matches()) return false; } + return true; + } + + private static void onDungeonStart() { + setCurrentFloor(); + isDungeonStarted = true; + puzzleCount = getPuzzleCount(); + isMayorPaul = Utils.getMayor().equals("Paul"); + } + + private static int calculateScore() { + int timeScore = calculateTimeScore(); + int exploreScore = calculateExploreScore(); + int skillScore = calculateSkillScore(); + int paulScore = isMayorPaul ? 10 : 0; + int cryptsScore = Math.min(getCrypts(), 5); + int mimicScore = isMimicKilled ? 2 : 0; + int totalScore = timeScore + exploreScore + skillScore + paulScore + cryptsScore + mimicScore; + //Will be this way until ready for pr, so it's easy to debug. + LOGGER.info("Total Score: {} (Time: {}, Explore: {}, Skill: {}, Paul: {}, Crypts: {}, Mimic: {})", totalScore, timeScore, exploreScore, skillScore, paulScore, cryptsScore, mimicScore); + return totalScore; + } + + private static int calculateExploreScore() { + int completedRoomScore = (int) Math.floor(60.0 * getCompletedRooms() / getTotalRooms()); + int percentageRequirement = floorRequirements.get(currentFloor).percentage; + int secretsScore = (int) Math.floor(40 * Math.min(percentageRequirement, getSecretsPercentage()) / percentageRequirement); + return completedRoomScore + secretsScore; + } + + private static int calculateTimeScore() { + Matcher timeMatcher = PlayerListMgr.regexAt(45, TIME_PATTERN); + if (timeMatcher == null) { + LOGGER.error("Time pattern doesn't match"); + return 0; + } + int score = 100; + int hours = Optional.ofNullable(timeMatcher.group("hours")).map(Integer::parseInt).orElse(0); + int minutes = Optional.ofNullable(timeMatcher.group("minutes")).map(Integer::parseInt).orElse(0); + int seconds = Optional.ofNullable(timeMatcher.group("seconds")).map(Integer::parseInt).orElse(0); + int timeSpent = hours * 3600 + minutes * 60 + seconds; + int timeRequirement = floorRequirements.get(currentFloor).timeLimit; + if (timeSpent < timeRequirement) return score; + + double timePastRequirement = ((double) (timeSpent - timeRequirement) / timeRequirement) * 100; + if (timePastRequirement >= 0 && timePastRequirement < 20) { + score -= (int) timePastRequirement / 2; + } else if (timePastRequirement >= 20 && timePastRequirement < 40) { + score -= (int) (10 + (timePastRequirement - 20) / 4); + } else if (timePastRequirement >= 40 && timePastRequirement < 50) { + score -= (int) (15 + (timePastRequirement - 40) / 5); + } else if (timePastRequirement >= 50 && timePastRequirement < 60) { + score -= (int) (17 + (timePastRequirement - 50) / 6); + } else if (timePastRequirement >= 60) { + score -= (int) (18 + (2.0 / 3.0) + (timePastRequirement - 60) / 7); + } + return score; + } + + private static int calculateSkillScore() { + return 20 + (int) Math.floor(80.0 * getCompletedRooms() / getTotalRooms()) - (2 * getDeathCount()) - (10 * getFailedPuzzles()); } private static void reset() { sent270 = false; sent300 = false; + isDungeonStarted = false; + isMimicKilled = false; + isMayorPaul = false; + puzzleCount = 0; + currentFloor = ""; + } + + public static void setMimicKilled(boolean killed) { + isMimicKilled = killed; + } + + private static int getTotalRooms() { + return (int) Math.round((getCompletedRooms()) / getClearPercentage()); //Clear% rounds to the closest integer so it can be off by 0.5% at most, this should be accurate enough + } + + private static int getCompletedRooms() { + Matcher completedRoomsMatcher = PlayerListMgr.regexAt(43, COMPLETED_ROOMS_PATTERN); + if (completedRoomsMatcher == null) { + LOGGER.error("Completed rooms pattern doesn't match"); + return 0; + } + return Integer.parseInt(completedRoomsMatcher.group("rooms")); + } + + private static double getClearPercentage() { + for (String sidebarLine : Utils.STRING_SCOREBOARD) { + Matcher clearMatcher = CLEARED_PATTERN.matcher(sidebarLine); + if (!clearMatcher.matches()) continue; + return Double.parseDouble(clearMatcher.group("cleared")) / 100; + } + LOGGER.error("Clear pattern doesn't match"); + return 0; + } + + private static int getDeathCount() { + Matcher matcher = PlayerListMgr.regexAt(25, DEATHS_PATTERN); + if (matcher == null) { + LOGGER.error("Death count pattern doesn't match"); + return 0; + } + //TODO: Turn this into a map of players and their deathcounts, get party members' pets, check if they have spirit pet, if they have it reduce their death count by 0.5 + return Integer.parseInt(matcher.group("deaths")); + } + + private static int getPuzzleCount() { + Matcher matcher = PlayerListMgr.regexAt(47, PUZZLE_COUNT_PATTERN); + if (matcher == null) { + LOGGER.error("Puzzle count pattern doesn't match"); + return 0; + } + return Integer.parseInt(matcher.group("count")); + } + + //Might be replaced to look for puzzle fail messages on chat instead of playerlist + private static int getFailedPuzzles() { + int failedPuzzles = 0; + for (int index = 0; index < puzzleCount; index++) { + Matcher puzzleMatcher = PlayerListMgr.regexAt(48 + index, PUZZLES_PATTERN); + if (puzzleMatcher == null) { + LOGGER.error("Puzzle pattern doesn't match"); + return 0; + } + if (puzzleMatcher.group("state").equals("✖")) failedPuzzles++; + } + return failedPuzzles; + } + + private static double getSecretsPercentage() { + Matcher matcher = PlayerListMgr.regexAt(44, SECRETS_PATTERN); + if (matcher == null) { + LOGGER.error("Secrets pattern doesn't match"); + return 0; + } + return Double.parseDouble(matcher.group("secper")); + } + + private static int getCrypts() { + Matcher matcher = PlayerListMgr.regexAt(33, CRYPTS_PATTERN); + if (matcher == null) { + LOGGER.error("Crypts pattern doesn't match"); + return 0; + } + return Integer.parseInt(matcher.group("crypts")); + } + + public static void setCurrentFloor() { + for (String sidebarLine : Utils.STRING_SCOREBOARD) { + Matcher floorMatcher = FLOOR_PATTERN.matcher(sidebarLine); + if (!floorMatcher.matches()) continue; + currentFloor = floorMatcher.group("floor"); + return; + } + LOGGER.error("Floor pattern doesn't match"); + } + + record FloorRequirement(int percentage, int timeLimit) { } } + diff --git a/src/main/java/de/hysky/skyblocker/skyblock/filters/MimicFilter.java b/src/main/java/de/hysky/skyblocker/skyblock/filters/MimicFilter.java new file mode 100644 index 00000000..0fce5b2c --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/filters/MimicFilter.java @@ -0,0 +1,26 @@ +package de.hysky.skyblocker.skyblock.filters; + +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.skyblock.dungeon.DungeonScore; +import de.hysky.skyblocker.utils.chat.ChatFilterResult; +import de.hysky.skyblocker.utils.chat.ChatPatternListener; +import net.minecraft.text.Text; + +import java.util.regex.Matcher; + +public class MimicFilter extends ChatPatternListener { + public MimicFilter() { + super("(?:Mimic dead!|Mimic Killed!|\\$SKYTILS-DUNGEON-SCORE-MIMIC\\$|\\Q" + SkyblockerConfigManager.get().locations.dungeons.mimicMessages.mimicMessage + "\\E)$"); + } + + @Override + public ChatFilterResult state() { + return SkyblockerConfigManager.get().messages.hideMimicKill; + } + + @Override + protected boolean onMatch(Text message, Matcher matcher) { + DungeonScore.setMimicKilled(true); + return false; + } +} diff --git a/src/main/java/de/hysky/skyblocker/utils/Utils.java b/src/main/java/de/hysky/skyblocker/utils/Utils.java index 45ace085..1fc718be 100644 --- a/src/main/java/de/hysky/skyblocker/utils/Utils.java +++ b/src/main/java/de/hysky/skyblocker/utils/Utils.java @@ -24,6 +24,7 @@ import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.IOException; import java.util.Collections; import java.util.List; @@ -64,6 +65,8 @@ public class Utils { private static boolean sentLocRaw = false; private static boolean canSendLocRaw = false; + private static String mayor = ""; + /** * @implNote The parent text will always be empty, the actual text content is inside the text's siblings. */ @@ -135,7 +138,16 @@ public class Utils { return map; } + /** + * @return the current mayor as cached on skyblock join. + */ + @NotNull + public static String getMayor() { + return mayor; + } + public static void init() { + SkyblockEvents.JOIN.register(Utils::initializeMayorCache); 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 @@ -381,4 +393,19 @@ public class Utils { locationRaw = ""; map = ""; } + + private static void initializeMayorCache() { + if (!mayor.isEmpty()) return; + try { + JsonObject json = JsonParser.parseString(Http.sendGetRequest("https://api.hypixel.net/v2/resources/skyblock/election")).getAsJsonObject(); + if (json.get("success").getAsBoolean()) { + mayor = json.get("mayor").getAsJsonObject().get("name").getAsString(); + System.out.println(mayor); + } else { + throw new IOException("API call for mayor failed: " + json.get("cause").getAsString()); + } + } catch (IOException | InterruptedException e) { + e.printStackTrace(); + } + } } diff --git a/src/main/java/de/hysky/skyblocker/utils/chat/ChatMessageListener.java b/src/main/java/de/hysky/skyblocker/utils/chat/ChatMessageListener.java index ebdb6f09..97398625 100644 --- a/src/main/java/de/hysky/skyblocker/utils/chat/ChatMessageListener.java +++ b/src/main/java/de/hysky/skyblocker/utils/chat/ChatMessageListener.java @@ -1,7 +1,5 @@ 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; @@ -9,6 +7,8 @@ import de.hysky.skyblocker.skyblock.dungeon.puzzle.ThreeWeirdos; import de.hysky.skyblocker.skyblock.dungeon.puzzle.Trivia; import de.hysky.skyblocker.skyblock.dwarven.Fetchur; import de.hysky.skyblocker.skyblock.dwarven.Puzzler; +import de.hysky.skyblocker.skyblock.filters.*; +import de.hysky.skyblocker.utils.Utils; import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents; import net.fabricmc.fabric.api.event.Event; import net.fabricmc.fabric.api.event.EventFactory; @@ -54,7 +54,8 @@ public interface ChatMessageListener { new TeleportPadFilter(), new AutopetFilter(), new ShowOffFilter(), - new ToggleSkyMallFilter() + new ToggleSkyMallFilter(), + new MimicFilter() }; // Register all listeners to EVENT for (ChatMessageListener listener : listeners) { diff --git a/src/main/resources/assets/skyblocker/lang/en_us.json b/src/main/resources/assets/skyblocker/lang/en_us.json index dfc0c035..cd1d9673 100644 --- a/src/main/resources/assets/skyblocker/lang/en_us.json +++ b/src/main/resources/assets/skyblocker/lang/en_us.json @@ -231,6 +231,11 @@ "text.autoconfig.skyblocker.option.locations.dungeons.solveTicTacToe.@Tooltip": "Puts a red box around the next best move for you to make!", "text.autoconfig.skyblocker.option.locations.dungeons.solveWaterboard": "Solve Waterboard Puzzle", "text.autoconfig.skyblocker.option.locations.dungeons.solveWaterboard.@Tooltip": "Click the levers with green boxes to solve the puzzle.", + "text.autoconfig.skyblocker.option.locations.dungeons.mimicMessages": "Mimic Messages", + "text.autoconfig.skyblocker.option.locations.dungeons.mimicMessages.sendMimicMessages": "Enable Mimic Messages", + "text.autoconfig.skyblocker.option.locations.dungeons.mimicMessages.sendMimicMessages.@Tooltip": "Sends a message in chat when a mimic is killed for score calculation mods.", + "text.autoconfig.skyblocker.option.locations.dungeons.mimicMessages.mimicMessage": "Mimic Message", + "text.autoconfig.skyblocker.option.locations.dungeons.mimicMessages.mimicMessage.@Tooltip": "Message which will be sent in the chat when a mimic is killed. Recommended to keep as \"Mimic dead!\"", "text.autoconfig.skyblocker.option.locations.dungeons.lividColor": "Livid Color", "text.autoconfig.skyblocker.option.locations.dungeons.lividColor.enableLividColorGlow": "Enable Livid Color Glow", "text.autoconfig.skyblocker.option.locations.dungeons.lividColor.enableLividColorGlow.@Tooltip": "Applies the glowing effect to the correct Livid in F5/M5.", @@ -294,6 +299,8 @@ "text.autoconfig.skyblocker.option.messages.hideShowOff.@Tooltip": "Filters messages from the /show command", "text.autoconfig.skyblocker.option.messages.hideToggleSkyMall": "Hide Toggle Sky Mall Messages", "text.autoconfig.skyblocker.option.messages.hideToggleSkyMall.@Tooltip": "Hides those pesky messages telling you to disable the Sky Mall HOTM perk when you want it enabled!", + "text.autoconfig.skyblocker.option.messages.hideMimicKill": "Hide Mimic Kill Messages", + "text.autoconfig.skyblocker.option.messages.hideMimicKill.@Tooltip": "Filters the \"Mimic dead!\" and \"Mimic killed!\" messages from chat.", "text.autoconfig.skyblocker.category.slayer": "Slayers", "text.autoconfig.skyblocker.option.slayer.vampireSlayer": "Vampire Slayer", "text.autoconfig.skyblocker.option.slayer.vampireSlayer.enableEffigyWaypoints": "Enable Effigy Waypoints", -- cgit From 2b1239950626b4480b0ac7120f4c8429dfaf8b68 Mon Sep 17 00:00:00 2001 From: Emirlol <81419447+Emirlol@users.noreply.github.com> Date: Sun, 7 Jan 2024 02:58:21 +0300 Subject: Improved time score calculation --- .../de/hysky/skyblocker/skyblock/dungeon/DungeonScore.java | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonScore.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonScore.java index ebec99dc..6016813c 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonScore.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonScore.java @@ -35,7 +35,6 @@ public class DungeonScore { private static final Pattern SECRETS_PATTERN = Pattern.compile("Secrets Found: (?\\d+\\.?\\d*)%"); private static final Pattern PUZZLES_PATTERN = Pattern.compile(".+?(?=:): \\[(?.)](?: \\(\\w+\\))?"); private static final Pattern PUZZLE_COUNT_PATTERN = Pattern.compile("Puzzles: \\((?\\d+)\\)"); - private static final Pattern TIME_PATTERN = Pattern.compile("Time: (?:(?\\d+(?=h))?h? ?(?\\d+(?=m))?m? ?(?\\d+(?=s))s|Soon!)"); private static final Pattern CRYPTS_PATTERN = Pattern.compile("Crypts: (?\\d+)"); private static final Pattern COMPLETED_ROOMS_PATTERN = Pattern.compile(" *Completed Rooms: (?\\d+)"); private static final Pattern DUNGEON_START_PATTERN = Pattern.compile("(?:Auto-closing|Starting) in: \\d:\\d+"); @@ -49,6 +48,7 @@ public class DungeonScore { //Caching the dungeon start state to prevent unnecessary scoreboard pattern matching after dungeon starts private static boolean isDungeonStarted; private static boolean isMayorPaul; + private static long startingTime; private static final HashMap floorRequirements = new HashMap<>(Map.ofEntries( Map.entry("E", new FloorRequirement(30, 1200)), Map.entry("F1", new FloorRequirement(30, 600)), @@ -135,6 +135,7 @@ public class DungeonScore { isDungeonStarted = true; puzzleCount = getPuzzleCount(); isMayorPaul = Utils.getMayor().equals("Paul"); + startingTime = System.currentTimeMillis(); } private static int calculateScore() { @@ -158,16 +159,8 @@ public class DungeonScore { } private static int calculateTimeScore() { - Matcher timeMatcher = PlayerListMgr.regexAt(45, TIME_PATTERN); - if (timeMatcher == null) { - LOGGER.error("Time pattern doesn't match"); - return 0; - } int score = 100; - int hours = Optional.ofNullable(timeMatcher.group("hours")).map(Integer::parseInt).orElse(0); - int minutes = Optional.ofNullable(timeMatcher.group("minutes")).map(Integer::parseInt).orElse(0); - int seconds = Optional.ofNullable(timeMatcher.group("seconds")).map(Integer::parseInt).orElse(0); - int timeSpent = hours * 3600 + minutes * 60 + seconds; + int timeSpent = (int) (System.currentTimeMillis() - startingTime) / 1000; int timeRequirement = floorRequirements.get(currentFloor).timeLimit; if (timeSpent < timeRequirement) return score; -- cgit From 6a350f931609432bd93362b2dbdc26e23450e2b8 Mon Sep 17 00:00:00 2001 From: Emirlol <81419447+Emirlol@users.noreply.github.com> Date: Sun, 14 Jan 2024 18:21:20 +0300 Subject: Fixed most things and cleaned up code --- .../mixin/ClientPlayNetworkHandlerMixin.java | 19 +- .../skyblocker/skyblock/dungeon/DungeonScore.java | 596 ++++++++++++--------- src/main/java/de/hysky/skyblocker/utils/Utils.java | 1 - 3 files changed, 353 insertions(+), 263 deletions(-) diff --git a/src/main/java/de/hysky/skyblocker/mixin/ClientPlayNetworkHandlerMixin.java b/src/main/java/de/hysky/skyblocker/mixin/ClientPlayNetworkHandlerMixin.java index 4015dfa5..b3fc871b 100644 --- a/src/main/java/de/hysky/skyblocker/mixin/ClientPlayNetworkHandlerMixin.java +++ b/src/main/java/de/hysky/skyblocker/mixin/ClientPlayNetworkHandlerMixin.java @@ -1,19 +1,25 @@ package de.hysky.skyblocker.mixin; import com.llamalad7.mixinextras.injector.WrapWithCondition; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; import com.llamalad7.mixinextras.sugar.Local; import de.hysky.skyblocker.skyblock.FishingHelper; +import de.hysky.skyblocker.skyblock.dungeon.DungeonScore; import de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonManager; import de.hysky.skyblocker.skyblock.waypoint.MythologicalRitual; import de.hysky.skyblocker.utils.Utils; import net.minecraft.client.MinecraftClient; import net.minecraft.client.network.ClientPlayNetworkHandler; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityStatuses; import net.minecraft.entity.ItemEntity; import net.minecraft.entity.LivingEntity; +import net.minecraft.network.packet.s2c.play.EntityStatusS2CPacket; import net.minecraft.network.packet.s2c.play.ParticleS2CPacket; import net.minecraft.network.packet.s2c.play.PlaySoundS2CPacket; import net.minecraft.util.Identifier; - +import net.minecraft.world.World; import org.slf4j.Logger; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; @@ -49,12 +55,12 @@ public abstract class ClientPlayNetworkHandlerMixin { private boolean skyblocker$cancelTeamWarning(Logger instance, String format, Object... arg) { return !Utils.isOnHypixel(); } - + @WrapWithCondition(method = { "onScoreboardScoreUpdate", "onScoreboardScoreReset" }, at = @At(value = "INVOKE", target = "Lorg/slf4j/Logger;warn(Ljava/lang/String;Ljava/lang/Object;)V", remap = false)) private boolean skyblocker$cancelUnknownScoreboardObjectiveWarnings(Logger instance, String message, Object objectiveName) { return !Utils.isOnHypixel(); } - + @WrapWithCondition(method = "warnOnUnknownPayload", at = @At(value = "INVOKE", target = "Lorg/slf4j/Logger;warn(Ljava/lang/String;Ljava/lang/Object;)V", remap = false)) private boolean skyblocker$dropBadlionPacketWarnings(Logger instance, String message, Object identifier) { return !(Utils.isOnHypixel() && ((Identifier) identifier).getNamespace().equals("badlion")); @@ -64,4 +70,11 @@ public abstract class ClientPlayNetworkHandlerMixin { private void skyblocker$onParticle(ParticleS2CPacket packet, CallbackInfo ci) { MythologicalRitual.onParticle(packet); } + + @WrapOperation(method = "onEntityStatus", at = @At(value = "INVOKE", target = "Lnet/minecraft/network/packet/s2c/play/EntityStatusS2CPacket;getEntity(Lnet/minecraft/world/World;)Lnet/minecraft/entity/Entity;")) + private Entity skyblocker$onEntityDeath(EntityStatusS2CPacket packet, World world, Operation original) { + Entity entity = original.call(packet, world); + if (packet.getStatus() == EntityStatuses.PLAY_DEATH_SOUND_OR_ADD_PROJECTILE_HIT_PARTICLES) DungeonScore.handleEntityDeath(entity); + return entity; + } } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonScore.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonScore.java index 6016813c..0334290d 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonScore.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonScore.java @@ -1,21 +1,26 @@ package de.hysky.skyblocker.skyblock.dungeon; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import de.hysky.skyblocker.config.SkyblockerConfig; import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.events.SkyblockEvents; +import de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonManager; import de.hysky.skyblocker.skyblock.tabhud.util.PlayerListMgr; +import de.hysky.skyblocker.utils.ApiUtils; import de.hysky.skyblocker.utils.Constants; +import de.hysky.skyblocker.utils.Http; import de.hysky.skyblocker.utils.Utils; import de.hysky.skyblocker.utils.scheduler.MessageScheduler; import de.hysky.skyblocker.utils.scheduler.Scheduler; +import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents; import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; -import net.fabricmc.fabric.api.entity.event.v1.ServerLivingEntityEvents; import net.minecraft.client.MinecraftClient; import net.minecraft.entity.Entity; import net.minecraft.entity.mob.ZombieEntity; import net.minecraft.item.ItemStack; -import net.minecraft.nbt.NbtCompound; -import net.minecraft.nbt.NbtElement; import net.minecraft.sound.SoundEvents; import net.minecraft.util.collection.DefaultedList; import org.slf4j.Logger; @@ -23,265 +28,338 @@ import org.slf4j.LoggerFactory; import java.util.HashMap; import java.util.Map; -import java.util.Optional; +import java.util.concurrent.CompletableFuture; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.StreamSupport; public class DungeonScore { - private static final SkyblockerConfig.DungeonScore SCORE_CONFIG = SkyblockerConfigManager.get().locations.dungeons.dungeonScore; - private static final SkyblockerConfig.MimicMessages MIMIC_MESSAGES_CONFIG = SkyblockerConfigManager.get().locations.dungeons.mimicMessages; - private static final Logger LOGGER = LoggerFactory.getLogger("Skyblocker Dungeon Score"); - private static final Pattern CLEARED_PATTERN = Pattern.compile("Cleared: (?\\d+)%.*"); - private static final Pattern SECRETS_PATTERN = Pattern.compile("Secrets Found: (?\\d+\\.?\\d*)%"); - private static final Pattern PUZZLES_PATTERN = Pattern.compile(".+?(?=:): \\[(?.)](?: \\(\\w+\\))?"); - private static final Pattern PUZZLE_COUNT_PATTERN = Pattern.compile("Puzzles: \\((?\\d+)\\)"); - private static final Pattern CRYPTS_PATTERN = Pattern.compile("Crypts: (?\\d+)"); - private static final Pattern COMPLETED_ROOMS_PATTERN = Pattern.compile(" *Completed Rooms: (?\\d+)"); - private static final Pattern DUNGEON_START_PATTERN = Pattern.compile("(?:Auto-closing|Starting) in: \\d:\\d+"); - private static final Pattern FLOOR_PATTERN = Pattern.compile(".*?(?=T)The Catacombs \\((?[EFM]\\D*\\d*)\\)"); - private static final Pattern DEATHS_PATTERN = Pattern.compile("Team Deaths: (?\\d+)"); - private static String currentFloor; - private static boolean sent270; - private static boolean sent300; - private static boolean isMimicKilled; - private static int puzzleCount; - //Caching the dungeon start state to prevent unnecessary scoreboard pattern matching after dungeon starts - private static boolean isDungeonStarted; - private static boolean isMayorPaul; - private static long startingTime; - private static final HashMap floorRequirements = new HashMap<>(Map.ofEntries( - Map.entry("E", new FloorRequirement(30, 1200)), - Map.entry("F1", new FloorRequirement(30, 600)), - Map.entry("F2", new FloorRequirement(40, 600)), - Map.entry("F3", new FloorRequirement(50, 600)), - Map.entry("F4", new FloorRequirement(60, 720)), - Map.entry("F5", new FloorRequirement(70, 600)), - Map.entry("F6", new FloorRequirement(85, 720)), - Map.entry("F7", new FloorRequirement(100, 840)), - Map.entry("M1", new FloorRequirement(100, 480)), - Map.entry("M2", new FloorRequirement(100, 480)), - Map.entry("M3", new FloorRequirement(100, 480)), - Map.entry("M4", new FloorRequirement(100, 480)), - Map.entry("M5", new FloorRequirement(100, 480)), - Map.entry("M6", new FloorRequirement(100, 600)), - Map.entry("M7", new FloorRequirement(100, 840)) - )); - - public static void init() { - Scheduler.INSTANCE.scheduleCyclic(DungeonScore::tick, 20); - ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> reset()); - ServerLivingEntityEvents.AFTER_DEATH.register((entity, source) -> { - if (isEntityMimic(entity)) { - if (MIMIC_MESSAGES_CONFIG.sendMimicMessages) MessageScheduler.INSTANCE.sendMessageAfterCooldown(MIMIC_MESSAGES_CONFIG.mimicMessage); - setMimicKilled(true); - } - }); - } - - public static void tick() { - MinecraftClient client = MinecraftClient.getInstance(); - if (!Utils.isInDungeons() || client.player == null) { - reset(); - return; - } - if (!isDungeonStarted) { - if (checkIfDungeonStarted()) onDungeonStart(); - return; - } - int score = calculateScore(); - if (SCORE_CONFIG.enableDungeonScore270 && !sent270 && score >= 270 && score < 300) { - MessageScheduler.INSTANCE.sendMessageAfterCooldown("/pc " + Constants.PREFIX.get().getString() + SCORE_CONFIG.dungeonScore270Message.replaceAll("\\[score]", "270")); - client.player.playSound(SoundEvents.BLOCK_NOTE_BLOCK_PLING.value(), 1f, 0.1f); - sent270 = true; - } - if (SCORE_CONFIG.enableDungeonScore300 && !sent300 && score >= 300) { - MessageScheduler.INSTANCE.sendMessageAfterCooldown("/pc " + Constants.PREFIX.get().getString() + SCORE_CONFIG.dungeonScore300Message.replaceAll("\\[score]", "300")); - client.player.playSound(SoundEvents.BLOCK_NOTE_BLOCK_PLING.value(), 1f, 1f); - sent300 = true; - } - } - - public static boolean isEntityMimic(Entity entity) { - if (!(entity instanceof ZombieEntity zombie)) return false; - if (!zombie.isBaby()) return false; - try { - DefaultedList armor = (DefaultedList) zombie.getArmorItems(); - if (armor.isEmpty()) return false; - NbtCompound helmetNbt = armor.get(3).getNbt(); - if (helmetNbt == null) return false; - return helmetNbt.getCompound("SkullOwner") - .getCompound("Properties") - .getList("textures", NbtElement.COMPOUND_TYPE) - .getCompound(0).getString("Value") - .equals("eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZTE5YzEyNTQzYmM3NzkyNjA1ZWY2OGUxZjg3NDlhZThmMmEzODFkOTA4NWQ0ZDRiNzgwYmExMjgyZDM1OTdhMCJ9fX0K"); - } catch (NullPointerException e) { - return false; - } catch (ClassCastException f) { - f.printStackTrace(); - return false; - } - } - - private static boolean checkIfDungeonStarted() { - for (String sidebarLine : Utils.STRING_SCOREBOARD) { - Matcher matcher = DUNGEON_START_PATTERN.matcher(sidebarLine); - if (matcher.matches()) return false; - } - return true; - } - - private static void onDungeonStart() { - setCurrentFloor(); - isDungeonStarted = true; - puzzleCount = getPuzzleCount(); - isMayorPaul = Utils.getMayor().equals("Paul"); - startingTime = System.currentTimeMillis(); - } - - private static int calculateScore() { - int timeScore = calculateTimeScore(); - int exploreScore = calculateExploreScore(); - int skillScore = calculateSkillScore(); - int paulScore = isMayorPaul ? 10 : 0; - int cryptsScore = Math.min(getCrypts(), 5); - int mimicScore = isMimicKilled ? 2 : 0; - int totalScore = timeScore + exploreScore + skillScore + paulScore + cryptsScore + mimicScore; - //Will be this way until ready for pr, so it's easy to debug. - LOGGER.info("Total Score: {} (Time: {}, Explore: {}, Skill: {}, Paul: {}, Crypts: {}, Mimic: {})", totalScore, timeScore, exploreScore, skillScore, paulScore, cryptsScore, mimicScore); - return totalScore; - } - - private static int calculateExploreScore() { - int completedRoomScore = (int) Math.floor(60.0 * getCompletedRooms() / getTotalRooms()); - int percentageRequirement = floorRequirements.get(currentFloor).percentage; - int secretsScore = (int) Math.floor(40 * Math.min(percentageRequirement, getSecretsPercentage()) / percentageRequirement); - return completedRoomScore + secretsScore; - } - - private static int calculateTimeScore() { - int score = 100; - int timeSpent = (int) (System.currentTimeMillis() - startingTime) / 1000; - int timeRequirement = floorRequirements.get(currentFloor).timeLimit; - if (timeSpent < timeRequirement) return score; - - double timePastRequirement = ((double) (timeSpent - timeRequirement) / timeRequirement) * 100; - if (timePastRequirement >= 0 && timePastRequirement < 20) { - score -= (int) timePastRequirement / 2; - } else if (timePastRequirement >= 20 && timePastRequirement < 40) { - score -= (int) (10 + (timePastRequirement - 20) / 4); - } else if (timePastRequirement >= 40 && timePastRequirement < 50) { - score -= (int) (15 + (timePastRequirement - 40) / 5); - } else if (timePastRequirement >= 50 && timePastRequirement < 60) { - score -= (int) (17 + (timePastRequirement - 50) / 6); - } else if (timePastRequirement >= 60) { - score -= (int) (18 + (2.0 / 3.0) + (timePastRequirement - 60) / 7); - } - return score; - } - - private static int calculateSkillScore() { - return 20 + (int) Math.floor(80.0 * getCompletedRooms() / getTotalRooms()) - (2 * getDeathCount()) - (10 * getFailedPuzzles()); - } - - private static void reset() { - sent270 = false; - sent300 = false; - isDungeonStarted = false; - isMimicKilled = false; - isMayorPaul = false; - puzzleCount = 0; - currentFloor = ""; - } - - public static void setMimicKilled(boolean killed) { - isMimicKilled = killed; - } - - private static int getTotalRooms() { - return (int) Math.round((getCompletedRooms()) / getClearPercentage()); //Clear% rounds to the closest integer so it can be off by 0.5% at most, this should be accurate enough - } - - private static int getCompletedRooms() { - Matcher completedRoomsMatcher = PlayerListMgr.regexAt(43, COMPLETED_ROOMS_PATTERN); - if (completedRoomsMatcher == null) { - LOGGER.error("Completed rooms pattern doesn't match"); - return 0; - } - return Integer.parseInt(completedRoomsMatcher.group("rooms")); - } - - private static double getClearPercentage() { - for (String sidebarLine : Utils.STRING_SCOREBOARD) { - Matcher clearMatcher = CLEARED_PATTERN.matcher(sidebarLine); - if (!clearMatcher.matches()) continue; - return Double.parseDouble(clearMatcher.group("cleared")) / 100; - } - LOGGER.error("Clear pattern doesn't match"); - return 0; - } - - private static int getDeathCount() { - Matcher matcher = PlayerListMgr.regexAt(25, DEATHS_PATTERN); - if (matcher == null) { - LOGGER.error("Death count pattern doesn't match"); - return 0; - } - //TODO: Turn this into a map of players and their deathcounts, get party members' pets, check if they have spirit pet, if they have it reduce their death count by 0.5 - return Integer.parseInt(matcher.group("deaths")); - } - - private static int getPuzzleCount() { - Matcher matcher = PlayerListMgr.regexAt(47, PUZZLE_COUNT_PATTERN); - if (matcher == null) { - LOGGER.error("Puzzle count pattern doesn't match"); - return 0; - } - return Integer.parseInt(matcher.group("count")); - } - - //Might be replaced to look for puzzle fail messages on chat instead of playerlist - private static int getFailedPuzzles() { - int failedPuzzles = 0; - for (int index = 0; index < puzzleCount; index++) { - Matcher puzzleMatcher = PlayerListMgr.regexAt(48 + index, PUZZLES_PATTERN); - if (puzzleMatcher == null) { - LOGGER.error("Puzzle pattern doesn't match"); - return 0; - } - if (puzzleMatcher.group("state").equals("✖")) failedPuzzles++; - } - return failedPuzzles; - } - - private static double getSecretsPercentage() { - Matcher matcher = PlayerListMgr.regexAt(44, SECRETS_PATTERN); - if (matcher == null) { - LOGGER.error("Secrets pattern doesn't match"); - return 0; - } - return Double.parseDouble(matcher.group("secper")); - } - - private static int getCrypts() { - Matcher matcher = PlayerListMgr.regexAt(33, CRYPTS_PATTERN); - if (matcher == null) { - LOGGER.error("Crypts pattern doesn't match"); - return 0; - } - return Integer.parseInt(matcher.group("crypts")); - } - - public static void setCurrentFloor() { - for (String sidebarLine : Utils.STRING_SCOREBOARD) { - Matcher floorMatcher = FLOOR_PATTERN.matcher(sidebarLine); - if (!floorMatcher.matches()) continue; - currentFloor = floorMatcher.group("floor"); - return; - } - LOGGER.error("Floor pattern doesn't match"); - } - - record FloorRequirement(int percentage, int timeLimit) { - } + private static final SkyblockerConfig.DungeonScore SCORE_CONFIG = SkyblockerConfigManager.get().locations.dungeons.dungeonScore; + private static final SkyblockerConfig.MimicMessages MIMIC_MESSAGES_CONFIG = SkyblockerConfigManager.get().locations.dungeons.mimicMessages; + private static final Logger LOGGER = LoggerFactory.getLogger("Skyblocker Dungeon Score"); + //Scoreboard patterns + private static final Pattern CLEARED_PATTERN = Pattern.compile("Cleared: (?\\d+)%.*"); + private static final Pattern DUNGEON_START_PATTERN = Pattern.compile("(?:Auto-closing|Starting) in: \\d:\\d+"); + private static final Pattern FLOOR_PATTERN = Pattern.compile(".*?(?=T)The Catacombs \\((?[EFM]\\D*\\d*)\\)"); + //Playerlist patterns + private static final Pattern SECRETS_PATTERN = Pattern.compile("Secrets Found: (?\\d+\\.?\\d*)%"); + private static final Pattern PUZZLES_PATTERN = Pattern.compile(".+?(?=:): \\[(?.)](?: \\(\\w+\\))?"); + private static final Pattern PUZZLE_COUNT_PATTERN = Pattern.compile("Puzzles: \\((?\\d+)\\)"); + private static final Pattern CRYPTS_PATTERN = Pattern.compile("Crypts: (?\\d+)"); + private static final Pattern COMPLETED_ROOMS_PATTERN = Pattern.compile(" *Completed Rooms: (?\\d+)"); + //Chat patterns + private static final Pattern DEATHS_PATTERN = Pattern.compile(".*?\u2620 (?\\S+) .*"); + //Other patterns + private static final Pattern MIMIC_FLOOR_FILTER_PATTERN = Pattern.compile("[EFM][12345]?"); + + private static String currentFloor; + private static boolean sent270; + private static boolean sent300; + private static boolean isMimicKilled; + private static int puzzleCount; + private static boolean isDungeonStarted; + private static boolean isMayorPaul; + private static long startingTime; + private static int deathCount; + private static boolean firstDeathHasSpiritPet; + private static boolean bloodRoomCompleted; + private static final Map SpiritPetCache = new HashMap<>(); + + public static void init() { + Scheduler.INSTANCE.scheduleCyclic(DungeonScore::tick, 20); + SkyblockEvents.LEAVE.register(SpiritPetCache::clear); + ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> reset()); + ClientReceiveMessageEvents.GAME.register((message, overlay) -> { + String str = message.getString(); + checkMessageForDeaths(str); + checkMessageForWatcher(str); + }); + } + + public static void tick() { + MinecraftClient client = MinecraftClient.getInstance(); + if (!Utils.isInDungeons() || client.player == null) { + reset(); + return; + } + if (!isDungeonStarted) { + if (checkIfDungeonStarted()) onDungeonStart(); + return; + } + int score = calculateScore(); + if (!sent270 && score >= 270 && score < 300) { + if (SCORE_CONFIG.enableDungeonScore270Message) { + MessageScheduler.INSTANCE.sendMessageAfterCooldown(SCORE_CONFIG.dungeonScore270Message.replaceAll("\\[score]", "270")); + } + if (SCORE_CONFIG.enableDungeonScore270Title) { + client.inGameHud.setDefaultTitleFade(); + client.inGameHud.setTitle(Constants.PREFIX.get().append(SCORE_CONFIG.dungeonScore270Message.replaceAll("\\[score]", "270"))); + } + if (SCORE_CONFIG.enableDungeonScore270Sound) { + client.player.playSound(SoundEvents.BLOCK_NOTE_BLOCK_PLING.value(), 100f, 0.1f); + } + sent270 = true; + } + if (!sent300 && score >= 300) { + if (SCORE_CONFIG.enableDungeonScore300Message) { + MessageScheduler.INSTANCE.sendMessageAfterCooldown(SCORE_CONFIG.dungeonScore300Message.replaceAll("\\[score]", "300")); + } + if (SCORE_CONFIG.enableDungeonScore300Title) { + client.inGameHud.setDefaultTitleFade(); + client.inGameHud.setTitle(Constants.PREFIX.get().append(SCORE_CONFIG.dungeonScore300Message.replaceAll("\\[score]", "300"))); + } + if (SCORE_CONFIG.enableDungeonScore300Sound) { + client.player.playSound(SoundEvents.BLOCK_NOTE_BLOCK_PLING.value(), 100f, 0.1f); + } + sent300 = true; + } + } + + private static void reset() { + sent270 = false; + sent300 = false; + isDungeonStarted = false; + isMimicKilled = false; + isMayorPaul = false; + firstDeathHasSpiritPet = false; + deathCount = 0; + currentFloor = ""; + } + + private static void onDungeonStart() { + setCurrentFloor(); + isDungeonStarted = true; + puzzleCount = getPuzzleCount(); + isMayorPaul = Utils.getMayor().equals("Paul"); + startingTime = System.currentTimeMillis(); + } + + private static int calculateScore() { + int timeScore = calculateTimeScore(); + int exploreScore = calculateExploreScore(); + int skillScore = calculateSkillScore(); + int bonusScore = calculateBonusScore(); + int totalScore = timeScore + exploreScore + skillScore + bonusScore; + if (currentFloor.equals("E")) totalScore = (int) (totalScore * 0.7); + //Will be this way until ready for pr, so it's easy to debug. + LOGGER.info("Total Score: {} (Time: {}, Explore: {}, Skill: {}, Bonus: {})", totalScore, timeScore, exploreScore, skillScore, bonusScore); + return totalScore; + } + + private static int calculateSkillScore() { + int extraCompletedRooms = 0; //This is needed for calculating the score before going in, so we have the result sooner + if (!DungeonManager.isInBoss()) extraCompletedRooms = bloodRoomCompleted ? 1 : 2; + return 20 + (int) Math.floor(80.0 * (getCompletedRooms() + extraCompletedRooms) / getTotalRooms()) - getPuzzlePenalty() - getDeathScorePenalty(); + } + + private static int calculateExploreScore() { + int extraCompletedRooms = 0; + if (!DungeonManager.isInBoss()) extraCompletedRooms = bloodRoomCompleted ? 1 : 2; + int completedRoomScore = (int) Math.floor(60.0 * (getCompletedRooms() + extraCompletedRooms) / getTotalRooms()); + int percentageRequirement = FloorRequirement.valueOf(currentFloor).percentage; + int secretsScore = (int) Math.floor(40 * Math.min(percentageRequirement, getSecretsPercentage()) / percentageRequirement); + return completedRoomScore + secretsScore; + } + + private static int calculateTimeScore() { + int score = 100; + int timeSpent = (int) (System.currentTimeMillis() - startingTime) / 1000; + int timeRequirement = FloorRequirement.valueOf(currentFloor).timeLimit; + if (timeSpent < timeRequirement) return score; + + double timePastRequirement = ((double) (timeSpent - timeRequirement) / timeRequirement) * 100; + if (timePastRequirement >= 0 && timePastRequirement < 20) { + score -= (int) timePastRequirement / 2; + } else if (timePastRequirement >= 20 && timePastRequirement < 40) { + score -= (int) (10 + (timePastRequirement - 20) / 4); + } else if (timePastRequirement >= 40 && timePastRequirement < 50) { + score -= (int) (15 + (timePastRequirement - 40) / 5); + } else if (timePastRequirement >= 50 && timePastRequirement < 60) { + score -= (int) (17 + (timePastRequirement - 50) / 6); + } else if (timePastRequirement >= 60) { + score -= (int) (18 + (2.0 / 3.0) + (timePastRequirement - 60) / 7); + } + return score; + } + + private static int calculateBonusScore() { + int paulScore = isMayorPaul ? 10 : 0; + int cryptsScore = Math.min(getCrypts(), 5); + int mimicScore = isMimicKilled ? 2 : 0; + if (getSecretsPercentage() >= 100 && !MIMIC_FLOOR_FILTER_PATTERN.matcher(currentFloor).matches()) mimicScore = 2; //If mimic kill is not announced but all secrets are found, mimic must've been killed + return paulScore + cryptsScore + mimicScore; + } + + private static boolean checkIfDungeonStarted() { + return Utils.STRING_SCOREBOARD.stream().anyMatch(s -> DUNGEON_START_PATTERN.matcher(s).matches()); + } + + public static boolean isEntityMimic(Entity entity) { + if (!Utils.isInDungeons()) return false; + if (MIMIC_FLOOR_FILTER_PATTERN.matcher(currentFloor).matches()) return false; + if (entity == null) return false; + if (!(entity instanceof ZombieEntity zombie)) return false; + if (!zombie.isBaby()) return false; + try { + DefaultedList armor = (DefaultedList) zombie.getArmorItems(); + return armor.stream().allMatch(ItemStack::isEmpty); + } catch (NullPointerException e) { + return false; + } catch (ClassCastException f) { + f.printStackTrace(); + return false; + } + } + + public static void handleEntityDeath(Entity entity) { + if (isMimicKilled) return; + if (!isEntityMimic(entity)) return; + isMimicKilled = true; + } + + public static void setMimicKilled(boolean state) { + isMimicKilled = state; + } + + private static int getTotalRooms() { + int completedRooms = getCompletedRooms(); + return (int) Math.round(completedRooms / getClearPercentage()); + } + + private static int getCompletedRooms() { + Matcher matcher = PlayerListMgr.regexAt(43, COMPLETED_ROOMS_PATTERN); + return matcher != null ? Integer.parseInt(matcher.group("rooms")) : 0; + } + + private static double getClearPercentage() { + for (String sidebarLine : Utils.STRING_SCOREBOARD) { + Matcher clearMatcher = CLEARED_PATTERN.matcher(sidebarLine); + if (!clearMatcher.matches()) continue; + return Double.parseDouble(clearMatcher.group("cleared")) / 100.0; + } + LOGGER.error("Clear pattern doesn't match"); + return 0; + } + + private static int getDeathScorePenalty() { + return deathCount * 2 - (firstDeathHasSpiritPet ? 1 : 0); + } + + private static int getPuzzleCount() { + Matcher matcher = PlayerListMgr.regexAt(47, PUZZLE_COUNT_PATTERN); + return matcher != null ? Integer.parseInt(matcher.group("count")) : 0; + } + + //Possible states: ✖, ✦, ✔ + private static int getPuzzlePenalty() { + int incompletePuzzles = 0; + for (int index = 0; index < puzzleCount; index++) { + Matcher puzzleMatcher = PlayerListMgr.regexAt(48 + index, PUZZLES_PATTERN); + if (puzzleMatcher == null) break; + if (puzzleMatcher.group("state").matches("[✖✦]")) incompletePuzzles++; + } + return incompletePuzzles * 10; + } + + private static double getSecretsPercentage() { + Matcher matcher = PlayerListMgr.regexAt(44, SECRETS_PATTERN); + return matcher != null ? Double.parseDouble(matcher.group("secper")) : 0; + } + + private static int getCrypts() { + Matcher matcher = PlayerListMgr.regexAt(33, CRYPTS_PATTERN); + if (matcher == null) matcher = PlayerListMgr.regexAt(32, CRYPTS_PATTERN); //If class milestone 9 is reached, crypts goes up by 1 + return matcher != null ? Integer.parseInt(matcher.group("crypts")) : 0; + } + + public static boolean hasSpiritPet(String name) { + return SpiritPetCache.computeIfAbsent(name, k -> { + String playeruuid = ApiUtils.name2Uuid(name); + try (Http.ApiResponse response = Http.sendHypixelRequest("skyblock/profiles", "?uuid=" + playeruuid)) { + if (!response.ok()) throw new IllegalStateException("Failed to get profile uuid for player " + name + "! Response: " + response.content()); + JsonObject responseJson = JsonParser.parseString(response.content()).getAsJsonObject(); + + JsonObject player = StreamSupport.stream(responseJson.getAsJsonArray("profiles").spliterator(), false) + .map(JsonElement::getAsJsonObject) + .filter(profile -> profile.getAsJsonPrimitive("selected").getAsBoolean()) + .findFirst() + .orElseThrow(() -> new IllegalStateException("No selected profile found!?")) + .getAsJsonObject("members").entrySet().stream() + .filter(entry -> entry.getKey().equals(playeruuid)) + .map(Map.Entry::getValue) + .map(JsonElement::getAsJsonObject) + .findFirst() + .orElseThrow(() -> new IllegalStateException("Player somehow not found inside their own profile!")); + + for (JsonElement element : player.getAsJsonObject("pets_data").getAsJsonArray("pets")) { + if (!element.getAsJsonObject().get("type").getAsString().equals("SPIRIT")) continue; + if (!element.getAsJsonObject().get("tier").getAsString().equals("LEGENDARY")) continue; + + return true; + } + } catch (Exception e) { + e.printStackTrace(); + LOGGER.error("[Skyblocker] Spirit pet lookup by name failed! Name: {} - Cause: {}", name, e.getMessage()); + } + return false; + }); + } + + private static void checkMessageForDeaths(String message) { + if (!Utils.isInDungeons()) return; + if (!message.startsWith("\u2620", 1)) return; + Matcher matcher = DEATHS_PATTERN.matcher(message); + if (!matcher.matches()) return; + deathCount++; + if (deathCount > 1) return; + final String whoDied = matcher.group("whodied").transform(s -> { + if (s.equals("You")) return MinecraftClient.getInstance().player.getName().getString(); //This will be wrong if the dead player is called 'You' but that's unlikely + else return s; + }); + CompletableFuture.supplyAsync(() -> hasSpiritPet(whoDied)) + .thenAccept(hasSpiritPet -> { + firstDeathHasSpiritPet = hasSpiritPet; + }); + } + + private static void checkMessageForWatcher(String m