aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory
diff options
context:
space:
mode:
authorKevin <92656833+kevinthegreat1@users.noreply.github.com>2024-05-23 22:19:13 -0400
committerGitHub <noreply@github.com>2024-05-23 22:19:13 -0400
commitb6da8de30929b4722244062e948dca42a810a230 (patch)
treef5ea29a18d285f29397d5e2a5bbaac06194eb38e /src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory
parent610c64758fc8d0b8bea0145c33881b60c0747bd7 (diff)
parent1f6798df58af5eec4dfe954ab413c5a92d0fee63 (diff)
downloadSkyblocker-b6da8de30929b4722244062e948dca42a810a230.tar.gz
Skyblocker-b6da8de30929b4722244062e948dca42a810a230.tar.bz2
Skyblocker-b6da8de30929b4722244062e948dca42a810a230.zip
Merge pull request #683 from Emirlol/chocolate-factory-helper
Add chocolate factory helper
Diffstat (limited to 'src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory')
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/ChocolateFactorySolver.java348
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/EggFinder.java168
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/TimeTowerReminder.java86
3 files changed, 602 insertions, 0 deletions
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/ChocolateFactorySolver.java b/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/ChocolateFactorySolver.java
new file mode 100644
index 00000000..2d530b6d
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/ChocolateFactorySolver.java
@@ -0,0 +1,348 @@
+package de.hysky.skyblocker.skyblock.chocolatefactory;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.skyblock.item.tooltip.ItemTooltip;
+import de.hysky.skyblocker.utils.ItemUtils;
+import de.hysky.skyblocker.utils.RegexUtils;
+import de.hysky.skyblocker.utils.render.gui.ColorHighlight;
+import de.hysky.skyblocker.utils.render.gui.ContainerSolver;
+import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
+import it.unimi.dsi.fastutil.objects.ObjectArrayList;
+import net.fabricmc.fabric.api.client.item.v1.ItemTooltipCallback;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.screen.ingame.GenericContainerScreen;
+import net.minecraft.client.item.TooltipType;
+import net.minecraft.item.Item;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.Items;
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.math.NumberUtils;
+
+import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.util.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class ChocolateFactorySolver extends ContainerSolver {
+ private static final Pattern CPS_PATTERN = Pattern.compile("([\\d,.]+) Chocolate per second");
+ private static final Pattern CPS_INCREASE_PATTERN = Pattern.compile("\\+([\\d,]+) Chocolate per second");
+ private static final Pattern COST_PATTERN = Pattern.compile("Cost ([\\d,]+) Chocolate");
+ private static final Pattern TOTAL_MULTIPLIER_PATTERN = Pattern.compile("Total Multiplier: ([\\d.]+)x");
+ private static final Pattern MULTIPLIER_INCREASE_PATTERN = Pattern.compile("\\+([\\d.]+)x Chocolate per second");
+ private static final Pattern CHOCOLATE_PATTERN = Pattern.compile("^([\\d,]+) Chocolate$");
+ private static final Pattern PRESTIGE_REQUIREMENT_PATTERN = Pattern.compile("Chocolate this Prestige: ([\\d,]+) +Requires (\\S+) Chocolate this Prestige!");
+ private static final Pattern TIME_TOWER_STATUS_PATTERN = Pattern.compile("Status: (ACTIVE|INACTIVE)");
+ private static final ObjectArrayList<Rabbit> cpsIncreaseFactors = new ObjectArrayList<>(6);
+ private static long totalChocolate = -1L;
+ private static double totalCps = -1.0;
+ private static double totalCpsMultiplier = -1.0;
+ private static long requiredUntilNextPrestige = -1L;
+ private static double timeTowerMultiplier = -1.0;
+ private static boolean isTimeTowerActive = false;
+ private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#,###.#", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
+ private static ItemStack bestUpgrade = null;
+ private static ItemStack bestAffordableUpgrade = null;
+
+ public ChocolateFactorySolver() {
+ super("^Chocolate Factory$");
+ ItemTooltipCallback.EVENT.register(ChocolateFactorySolver::handleTooltip);
+ }
+
+ @Override
+ protected boolean isEnabled() {
+ return SkyblockerConfigManager.get().helpers.chocolateFactory.enableChocolateFactoryHelper;
+ }
+
+ @Override
+ protected List<ColorHighlight> getColors(String[] groups, Int2ObjectMap<ItemStack> slots) {
+ updateFactoryInfo(slots);
+ List<ColorHighlight> highlights = new ArrayList<>();
+
+ getPrestigeHighlight(slots.get(28)).ifPresent(highlights::add);
+
+ if (totalChocolate <= 0 || cpsIncreaseFactors.isEmpty()) return highlights; //Something went wrong or there's nothing we can afford.
+ Rabbit bestRabbit = cpsIncreaseFactors.getFirst();
+ bestUpgrade = bestRabbit.itemStack;
+ if (bestRabbit.cost <= totalChocolate) {
+ highlights.add(ColorHighlight.green(bestRabbit.slot));
+ return highlights;
+ }
+ highlights.add(ColorHighlight.yellow(bestRabbit.slot));
+
+ for (Rabbit rabbit : cpsIncreaseFactors.subList(1, cpsIncreaseFactors.size())) {
+ if (rabbit.cost <= totalChocolate) {
+ bestAffordableUpgrade = rabbit.itemStack;
+ highlights.add(ColorHighlight.green(rabbit.slot));
+ break;
+ }
+ }
+
+ return highlights;
+ }
+
+ private static void updateFactoryInfo(Int2ObjectMap<ItemStack> slots) {
+ cpsIncreaseFactors.clear();
+
+ for (int i = 29; i <= 33; i++) { // The 5 rabbits slots are in 29, 30, 31, 32 and 33.
+ ItemStack item = slots.get(i);
+ if (item.isOf(Items.PLAYER_HEAD)) {
+ getRabbit(item, i).ifPresent(cpsIncreaseFactors::add);
+ }
+ }
+
+ //Coach is in slot 42
+ getCoach(slots.get(42)).ifPresent(cpsIncreaseFactors::add);
+
+ //The clickable chocolate is in slot 13, holds the total chocolate
+ RegexUtils.getLongFromMatcher(CHOCOLATE_PATTERN.matcher(slots.get(13).getName().getString())).ifPresent(l -> totalChocolate = l);
+
+ //Cps item (cocoa bean) is in slot 45
+ String cpsItemLore = getConcatenatedLore(slots.get(45));
+ Matcher cpsMatcher = CPS_PATTERN.matcher(cpsItemLore);
+ RegexUtils.getDoubleFromMatcher(cpsMatcher).ifPresent(d -> totalCps = d);
+ Matcher multiplierMatcher = TOTAL_MULTIPLIER_PATTERN.matcher(cpsItemLore);
+ RegexUtils.getDoubleFromMatcher(multiplierMatcher, cpsMatcher.hasMatch() ? cpsMatcher.end() : 0).ifPresent(d -> totalCpsMultiplier = d);
+
+ //Prestige item is in slot 28
+ Matcher prestigeMatcher = PRESTIGE_REQUIREMENT_PATTERN.matcher(getConcatenatedLore(slots.get(28)));
+ OptionalLong currentChocolate = RegexUtils.getLongFromMatcher(prestigeMatcher);
+ if (currentChocolate.isPresent()) {
+ String requirement = prestigeMatcher.group(2); //If the first one matched, we can assume the 2nd one is also matched since it's one whole regex
+ //Since the last character is either M or B we can just try to replace both characters. Only the correct one will actually replace anything.
+ String amountString = requirement.replace("M", "000000").replace("B", "000000000");
+ if (NumberUtils.isParsable(amountString)) {
+ requiredUntilNextPrestige = Long.parseLong(amountString) - currentChocolate.getAsLong();
+ }
+ }
+
+ //Time Tower is in slot 39
+ timeTowerMultiplier = romanToDecimal(StringUtils.substringAfterLast(slots.get(39).getName().getString(), ' ')) / 10.0; //The name holds the level, which is multiplier * 10 in roman numerals
+ Matcher timeTowerStatusMatcher = TIME_TOWER_STATUS_PATTERN.matcher(getConcatenatedLore(slots.get(39)));
+ if (timeTowerStatusMatcher.find()) {
+ isTimeTowerActive = timeTowerStatusMatcher.group(1).equals("ACTIVE");
+ }
+
+ //Compare cost/cpsIncrease rather than cpsIncrease/cost to avoid getting close to 0 and losing precision.
+ cpsIncreaseFactors.sort(Comparator.comparingDouble(rabbit -> rabbit.cost() / rabbit.cpsIncrease())); //Ascending order, lower = better
+ }
+
+ private static void handleTooltip(ItemStack stack, Item.TooltipContext tooltipContext, TooltipType tooltipType, List<Text> lines) {
+ if (!SkyblockerConfigManager.get().helpers.chocolateFactory.enableChocolateFactoryHelper) return;
+ if (!(MinecraftClient.getInstance().currentScreen instanceof GenericContainerScreen screen) || !screen.getTitle().getString().equals("Chocolate Factory")) return;
+
+ int lineIndex = lines.size();
+ //This boolean is used to determine if we should add a smooth line to separate the added information from the rest of the tooltip.
+ //It should be set to true if there's any information added, false otherwise.
+ boolean shouldAddLine = false;
+
+ String lore = concatenateLore(lines);
+ Matcher costMatcher = COST_PATTERN.matcher(lore);
+ OptionalLong cost = RegexUtils.getLongFromMatcher(costMatcher);
+ //Available on all items with a chocolate cost
+ if (cost.isPresent()) shouldAddLine = addUpgradeTimerToLore(lines, cost.getAsLong());
+
+ //Prestige item
+ if (stack.isOf(Items.DROPPER) && requiredUntilNextPrestige != -1L) {
+ shouldAddLine = addPrestigeTimerToLore(lines) || shouldAddLine;
+ }
+ //Time tower
+ else if (stack.isOf(Items.CLOCK)) {
+ shouldAddLine = addTimeTowerStatsToLore(lines) || shouldAddLine;
+ }
+ //Rabbits
+ else if (stack.isOf(Items.PLAYER_HEAD)) {
+ shouldAddLine = addRabbitStatsToLore(lines, stack) || shouldAddLine;
+ }
+
+ //This is an ArrayList, so this operation is probably not very efficient, but logically it's pretty much the only way I can think of
+ if (shouldAddLine) lines.add(lineIndex, ItemTooltip.createSmoothLine());
+ }
+
+ private static boolean addUpgradeTimerToLore(List<Text> lines, long cost) {
+ if (totalChocolate < 0L || totalCps < 0.0) return false;
+ lines.add(Text.empty()
+ .append(Text.literal("Time until upgrade: ").formatted(Formatting.GRAY))
+ .append(formatTime((cost - totalChocolate) / totalCps)));
+ return true;
+ }
+
+ private static boolean addPrestigeTimerToLore(List<Text> lines) {
+ if (requiredUntilNextPrestige == -1L || totalCps == -1.0) return false;
+ lines.add(Text.empty()
+ .append(Text.literal("Chocolate until next prestige: ").formatted(Formatting.GRAY))
+ .append(Text.literal(DECIMAL_FORMAT.format(requiredUntilNextPrestige)).formatted(Formatting.GOLD)));
+ lines.add(Text.empty()
+ .append(Text.literal("Time until next prestige: ").formatted(Formatting.GRAY))
+ .append(formatTime(requiredUntilNextPrestige / totalCps)));
+ return true;
+ }
+
+ private static boolean addTimeTowerStatsToLore(List<Text> lines) {
+ if (totalCps < 0.0 || totalCpsMultiplier < 0.0 || timeTowerMultiplier < 0.0) return false;
+ lines.add(Text.literal("Current stats:").formatted(Formatting.GRAY));
+ lines.add(Text.empty()
+ .append(Text.literal(" CPS increase: ").formatted(Formatting.GRAY))
+ .append(Text.literal(DECIMAL_FORMAT.format(totalCps / totalCpsMultiplier * timeTowerMultiplier)).formatted(Formatting.GOLD)));
+ lines.add(Text.empty()
+ .append(Text.literal(" CPS when active: ").formatted(Formatting.GRAY))
+ .append(Text.literal(DECIMAL_FORMAT.format(isTimeTowerActive ? totalCps : totalCps / totalCpsMultiplier * (timeTowerMultiplier + totalCpsMultiplier))).formatted(Formatting.GOLD)));
+ if (timeTowerMultiplier < 1.5) {
+ lines.add(Text.literal("Stats after upgrade:").formatted(Formatting.GRAY));
+ lines.add(Text.empty()
+ .append(Text.literal(" CPS increase: ").formatted(Formatting.GRAY))
+ .append(Text.literal(DECIMAL_FORMAT.format(totalCps / (totalCpsMultiplier) * (timeTowerMultiplier + 0.1))).formatted(Formatting.GOLD)));
+ lines.add(Text.empty()
+ .append(Text.literal(" CPS when active: ").formatted(Formatting.GRAY))
+ .append(Text.literal(DECIMAL_FORMAT.format(isTimeTowerActive ? totalCps / totalCpsMultiplier * (totalCpsMultiplier + 0.1) : totalCps / totalCpsMultiplier * (timeTowerMultiplier + 0.1 + totalCpsMultiplier))).formatted(Formatting.GOLD)));
+ }
+ return true;
+ }
+
+ private static boolean addRabbitStatsToLore(List<Text> lines, ItemStack stack) {
+ if (cpsIncreaseFactors.isEmpty()) return false;
+ boolean changed = false;
+ for (Rabbit rabbit : cpsIncreaseFactors) {
+ if (rabbit.itemStack != stack) continue;
+ changed = true;
+ lines.add(Text.empty()
+ .append(Text.literal("CPS Increase: ").formatted(Formatting.GRAY))
+ .append(Text.literal(DECIMAL_FORMAT.format(rabbit.cpsIncrease)).formatted(Formatting.GOLD)));
+
+ lines.add(Text.empty()
+ .append(Text.literal("Cost per CPS: ").formatted(Formatting.GRAY))
+ .append(Text.literal(DECIMAL_FORMAT.format(rabbit.cost / rabbit.cpsIncrease)).formatted(Formatting.GOLD)));
+
+ if (rabbit.itemStack == bestUpgrade) {
+ if (rabbit.cost <= totalChocolate) {
+ lines.add(Text.literal("Best upgrade").formatted(Formatting.GREEN));
+ } else {
+ lines.add(Text.literal("Best upgrade, can't afford").formatted(Formatting.YELLOW));
+ }
+ } else if (rabbit.itemStack == bestAffordableUpgrade && rabbit.cost <= totalChocolate) {
+ lines.add(Text.literal("Best upgrade you can afford").formatted(Formatting.GREEN));
+ }
+ }
+ return changed;
+ }
+
+ private static MutableText formatTime(double seconds) {
+ seconds = Math.ceil(seconds);
+ if (seconds <= 0) return Text.literal("Now").formatted(Formatting.GREEN);
+
+ StringBuilder builder = new StringBuilder();
+ if (seconds >= 86400) {
+ builder.append((int) (seconds / 86400)).append("d ");
+ seconds %= 86400;
+ }
+ if (seconds >= 3600) {
+ builder.append((int) (seconds / 3600)).append("h ");
+ seconds %= 3600;
+ }
+ if (seconds >= 60) {
+ builder.append((int) (seconds / 60)).append("m ");
+ seconds %= 60;
+ }
+ if (seconds >= 1) {
+ builder.append((int) seconds).append("s");
+ }
+ return Text.literal(builder.toString()).formatted(Formatting.GOLD);
+ }
+
+ /**
+ * Utility method.
+ */
+ private static String getConcatenatedLore(ItemStack item) {
+ return concatenateLore(ItemUtils.getLore(item));
+ }
+
+ /**
+ * Concatenates the lore of an item into one string.
+ * This is useful in case some pattern we're looking for is split into multiple lines, which would make it harder to regex.
+ */
+ private static String concatenateLore(List<Text> lore) {
+ StringBuilder stringBuilder = new StringBuilder();
+ for (int i = 0; i < lore.size(); i++) {
+ stringBuilder.append(lore.get(i).getString());
+ if (i != lore.size() - 1) stringBuilder.append(" ");
+ }
+ return stringBuilder.toString();
+ }
+
+ private static Optional<Rabbit> getCoach(ItemStack coachItem) {
+ if (!coachItem.isOf(Items.PLAYER_HEAD)) return Optional.empty();
+ String coachLore = getConcatenatedLore(coachItem);
+
+ if (totalCpsMultiplier == -1.0) return Optional.empty(); //We need the total multiplier to calculate the increase in cps.
+
+ Matcher multiplierIncreaseMatcher = MULTIPLIER_INCREASE_PATTERN.matcher(coachLore);
+ OptionalDouble currentCpsMultiplier = RegexUtils.getDoubleFromMatcher(multiplierIncreaseMatcher);
+ if (currentCpsMultiplier.isEmpty()) return Optional.empty();
+
+ OptionalDouble nextCpsMultiplier = RegexUtils.getDoubleFromMatcher(multiplierIncreaseMatcher);
+ if (nextCpsMultiplier.isEmpty()) { //This means that the coach isn't hired yet.
+ nextCpsMultiplier = currentCpsMultiplier; //So the first instance of the multiplier is actually the amount we'll get upon upgrading.
+ currentCpsMultiplier = OptionalDouble.of(0.0); //And so, we can re-assign values to the variables to make the calculation more readable.
+ }
+
+ Matcher costMatcher = COST_PATTERN.matcher(coachLore);
+ OptionalInt cost = RegexUtils.getIntFromMatcher(costMatcher, multiplierIncreaseMatcher.hasMatch() ? multiplierIncreaseMatcher.end() : 0); //Cost comes after the multiplier line
+ if (cost.isEmpty()) return Optional.empty();
+
+ return Optional.of(new Rabbit(totalCps / totalCpsMultiplier * (nextCpsMultiplier.getAsDouble() - currentCpsMultiplier.getAsDouble()), cost.getAsInt(), 42, coachItem));
+ }
+
+ private static Optional<Rabbit> getRabbit(ItemStack item, int slot) {
+ String lore = getConcatenatedLore(item);
+ Matcher cpsMatcher = CPS_INCREASE_PATTERN.matcher(lore);
+ OptionalInt currentCps = RegexUtils.getIntFromMatcher(cpsMatcher);
+ if (currentCps.isEmpty()) return Optional.empty();
+ OptionalInt nextCps = RegexUtils.getIntFromMatcher(cpsMatcher);
+ if (nextCps.isEmpty()) {
+ nextCps = currentCps; //This means that the rabbit isn't hired yet.
+ currentCps = OptionalInt.of(0); //So the first instance of the cps is actually the amount we'll get upon hiring.
+ }
+
+ Matcher costMatcher = COST_PATTERN.matcher(lore);
+ OptionalInt cost = RegexUtils.getIntFromMatcher(costMatcher, cpsMatcher.hasMatch() ? cpsMatcher.end() : 0); //Cost comes after the cps line
+ if (cost.isEmpty()) return Optional.empty();
+ return Optional.of(new Rabbit(nextCps.getAsInt() - currentCps.getAsInt(), cost.getAsInt(), slot, item));
+ }
+
+ private static Optional<ColorHighlight> getPrestigeHighlight(ItemStack item) {
+ List<Text> loreList = ItemUtils.getLore(item);
+ if (loreList.isEmpty()) return Optional.empty();
+
+ String lore = loreList.getLast().getString(); //The last line holds the text we're looking for
+ if (lore.equals("Click to prestige!")) return Optional.of(ColorHighlight.green(28));
+ return Optional.of(ColorHighlight.red(28));
+ }
+
+ private record Rabbit(double cpsIncrease, int cost, int slot, ItemStack itemStack) {
+ }
+
+ //Perhaps the part below can go to a separate file later on, but I couldn't find a proper name for the class, so they're staying here.
+ private static final Map<Character, Integer> romanMap = Map.of(
+ 'I', 1,
+ 'V', 5,
+ 'X', 10,
+ 'L', 50,
+ 'C', 100,
+ 'D', 500,
+ 'M', 1000
+ );
+
+ public static int romanToDecimal(String romanNumeral) {
+ int decimal = 0;
+ int lastNumber = 0;
+ for (int i = romanNumeral.length() - 1; i >= 0; i--) {
+ char ch = romanNumeral.charAt(i);
+ decimal = romanMap.get(ch) >= lastNumber ? decimal + romanMap.get(ch) : decimal - romanMap.get(ch);
+ lastNumber = romanMap.get(ch);
+ }
+ return decimal;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/EggFinder.java b/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/EggFinder.java
new file mode 100644
index 00000000..c4fd7d4d
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/EggFinder.java
@@ -0,0 +1,168 @@
+package de.hysky.skyblocker.skyblock.chocolatefactory;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.events.SkyblockEvents;
+import de.hysky.skyblocker.utils.*;
+import de.hysky.skyblocker.utils.waypoint.Waypoint;
+import it.unimi.dsi.fastutil.objects.ObjectImmutableList;
+import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents;
+import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents;
+import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext;
+import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.entity.Entity;
+import net.minecraft.entity.decoration.ArmorStandEntity;
+import net.minecraft.item.ItemStack;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import org.apache.commons.lang3.mutable.MutableObject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.LinkedList;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class EggFinder {
+ private static final Pattern eggFoundPattern = Pattern.compile("^(?:HOPPITY'S HUNT You found a Chocolate|You have already collected this Chocolate) (Breakfast|Lunch|Dinner)");
+ private static final Pattern newEggPattern = Pattern.compile("^HOPPITY'S HUNT A Chocolate (Breakfast|Lunch|Dinner) Egg has appeared!$");
+ private static final Logger logger = LoggerFactory.getLogger("Skyblocker Egg Finder");
+ private static final LinkedList<ArmorStandEntity> armorStandQueue = new LinkedList<>();
+ private static final Location[] possibleLocations = {Location.CRIMSON_ISLE, Location.CRYSTAL_HOLLOWS, Location.DUNGEON_HUB, Location.DWARVEN_MINES, Location.HUB, Location.THE_END, Location.THE_PARK, Location.GOLD_MINE};
+ private static boolean isLocationCorrect = false;
+
+ private EggFinder() {
+ }
+
+ public static void init() {
+ ClientPlayConnectionEvents.JOIN.register((ignored, ignored2, ignored3) -> invalidateState());
+ SkyblockEvents.LOCATION_CHANGE.register(EggFinder::handleLocationChange);
+ ClientReceiveMessageEvents.GAME.register(EggFinder::onChatMessage);
+ WorldRenderEvents.AFTER_TRANSLUCENT.register(EggFinder::renderWaypoints);
+ }
+
+ private static void handleLocationChange(Location location) {
+ for (Location possibleLocation : possibleLocations) {
+ if (location == possibleLocation) {
+ isLocationCorrect = true;
+ break;
+ }
+ }
+ if (!isLocationCorrect) {
+ armorStandQueue.clear();
+ return;
+ }
+
+ while (!armorStandQueue.isEmpty()) {
+ handleArmorStand(armorStandQueue.poll());
+ }
+ }
+
+ public static void checkIfEgg(Entity entity) {
+ if (entity instanceof ArmorStandEntity armorStand) checkIfEgg(armorStand);
+ }
+
+ public static void checkIfEgg(ArmorStandEntity armorStand) {
+ if (!SkyblockerConfigManager.get().helpers.chocolateFactory.enableEggFinder) return;
+ if (SkyblockTime.skyblockSeason.get() != SkyblockTime.Season.SPRING) return;
+ if (armorStand.hasCustomName() || !armorStand.isInvisible() || !armorStand.shouldHideBasePlate()) return;
+ if (Utils.getLocation() == Location.UNKNOWN) { //The location is unknown upon world change and will be changed via /locraw soon, so we can queue it for now
+ armorStandQueue.add(armorStand);
+ return;
+ }
+ if (isLocationCorrect) handleArmorStand(armorStand);
+ }
+
+ private static void handleArmorStand(ArmorStandEntity armorStand) {
+ for (ItemStack itemStack : armorStand.getArmorItems()) {
+ ItemUtils.getHeadTextureOptional(itemStack).ifPresent(texture -> {
+ for (EggType type : EggType.entries) { //Compare blockPos rather than entity to avoid incorrect matches when the entity just moves rather than a new one being spawned elsewhere
+ if (texture.equals(type.texture) && (type.egg.getValue() == null || !type.egg.getValue().entity.getBlockPos().equals(armorStand.getBlockPos()))) {
+ handleFoundEgg(armorStand, type);
+ return;
+ }
+ }
+ });
+ }
+ }
+
+ private static void invalidateState() {
+ if (!SkyblockerConfigManager.get().helpers.chocolateFactory.enableEggFinder) return;
+ isLocationCorrect = false;
+ for (EggType type : EggType.entries) {
+ type.egg.setValue(null);
+ }
+ }
+
+ private static void handleFoundEgg(ArmorStandEntity entity, EggType eggType) {
+ eggType.egg.setValue(new Egg(entity, new Waypoint(entity.getBlockPos().up(2), SkyblockerConfigManager.get().helpers.chocolateFactory.waypointType, ColorUtils.getFloatComponents(eggType.color))));
+
+ if (!SkyblockerConfigManager.get().helpers.chocolateFactory.sendEggFoundMessages) return;
+ MinecraftClient.getInstance().player.sendMessage(Constants.PREFIX.get()
+ .append("Found a ")
+ .append(Text.literal("Chocolate " + eggType + " Egg")
+ .withColor(eggType.color))
+ .append(" at " + entity.getBlockPos().up(2).toShortString() + "!"));
+ }
+
+ private static void renderWaypoints(WorldRenderContext context) {
+ if (!SkyblockerConfigManager.get().helpers.chocolateFactory.enableEggFinder) return;
+ for (EggType type : EggType.entries) {
+ Egg egg = type.egg.getValue();
+ if (egg != null && egg.waypoint.shouldRender()) egg.waypoint.render(context);
+ }
+ }
+
+ private static void onChatMessage(Text text, boolean overlay) {
+ if (overlay || !SkyblockerConfigManager.get().helpers.chocolateFactory.enableEggFinder) return;
+ Matcher matcher = eggFoundPattern.matcher(text.getString());
+ if (matcher.find()) {
+ try {
+ Egg egg = EggType.valueOf(matcher.group(1).toUpperCase()).egg.getValue();
+ if (egg != null) egg.waypoint.setFound();
+ } catch (IllegalArgumentException e) {
+ logger.error("[Skyblocker Egg Finder] Failed to find egg type for egg found message. Tried to match against: " + matcher.group(0), e);
+ }
+ }
+
+ //There's only one egg of the same type at any given time, so we can set the changed egg to null
+ matcher = newEggPattern.matcher(text.getString());
+ if (matcher.find()) {
+ try {
+ EggType.valueOf(matcher.group(1).toUpperCase()).egg.setValue(null);
+ } catch (IllegalArgumentException e) {
+ logger.error("[Skyblocker Egg Finder] Failed to find egg type for egg spawn message. Tried to match against: " + matcher.group(0), e);
+ }
+ }
+ }
+
+ record Egg(ArmorStandEntity entity, Waypoint waypoint) { }
+
+ enum EggType {
+ LUNCH(new MutableObject<>(), Formatting.BLUE.getColorValue(), "ewogICJ0aW1lc3RhbXAiIDogMTcxMTQ2MjU2ODExMiwKICAicHJvZmlsZUlkIiA6ICI3NzUwYzFhNTM5M2Q0ZWQ0Yjc2NmQ4ZGUwOWY4MjU0NiIsCiAgInByb2ZpbGVOYW1lIiA6ICJSZWVkcmVsIiwKICAic2lnbmF0dXJlUmVxdWlyZWQiIDogdHJ1ZSwKICAidGV4dHVyZXMiIDogewogICAgIlNLSU4iIDogewogICAgICAidXJsIiA6ICJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlLzdhZTZkMmQzMWQ4MTY3YmNhZjk1MjkzYjY4YTRhY2Q4NzJkNjZlNzUxZGI1YTM0ZjJjYmM2NzY2YTAzNTZkMGEiCiAgICB9CiAgfQp9"),
+ DINNER(new MutableObject<>(), Formatting.GREEN.getColorValue(), "ewogICJ0aW1lc3RhbXAiIDogMTcxMTQ2MjY0OTcwMSwKICAicHJvZmlsZUlkIiA6ICI3NGEwMzQxNWY1OTI0ZTA4YjMyMGM2MmU1NGE3ZjJhYiIsCiAgInByb2ZpbGVOYW1lIiA6ICJNZXp6aXIiLAogICJzaWduYXR1cmVSZXF1aXJlZCIgOiB0cnVlLAogICJ0ZXh0dXJlcyIgOiB7CiAgICAiU0tJTiIgOiB7CiAgICAgICJ1cmwiIDogImh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZTVlMzYxNjU4MTlmZDI4NTBmOTg1NTJlZGNkNzYzZmY5ODYzMTMxMTkyODNjMTI2YWNlMGM0Y2M0OTVlNzZhOCIKICAgIH0KICB9Cn0"),
+ BREAKFAST(new MutableObject<>(), Formatting.GOLD.getColorValue(), "ewogICJ0aW1lc3RhbXAiIDogMTcxMTQ2MjY3MzE0OSwKICAicHJvZmlsZUlkIiA6ICJiN2I4ZTlhZjEwZGE0NjFmOTY2YTQxM2RmOWJiM2U4OCIsCiAgInByb2ZpbGVOYW1lIiA6ICJBbmFiYW5hbmFZZzciLAogICJzaWduYXR1cmVSZXF1aXJlZCIgOiB0cnVlLAogICJ0ZXh0dXJlcyIgOiB7CiAgICAiU0tJTiIgOiB7CiAgICAgICJ1cmwiIDogImh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYTQ5MzMzZDg1YjhhMzE1ZDAzMzZlYjJkZjM3ZDhhNzE0Y2EyNGM1MWI4YzYwNzRmMWI1YjkyN2RlYjUxNmMyNCIKICAgIH0KICB9Cn0");
+
+ public final MutableObject<Egg> egg;
+ public final int color;
+ public final String texture;
+
+ //This is to not create an array each time we iterate over the values
+ public static final ObjectImmutableList<EggType> entries = ObjectImmutableList.of(BREAKFAST, LUNCH, DINNER);
+
+ EggType(MutableObject<Egg> egg, int color, String texture) {
+ this.egg = egg;
+ this.color = color;
+ this.texture = texture;
+ }
+
+ @Override
+ public String toString() {
+ return switch (this) {
+ case LUNCH -> "Lunch";
+ case DINNER -> "Dinner";
+ case BREAKFAST -> "Breakfast";
+ };
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/TimeTowerReminder.java b/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/TimeTowerReminder.java
new file mode 100644
index 00000000..72cbeb2a
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/TimeTowerReminder.java
@@ -0,0 +1,86 @@
+package de.hysky.skyblocker.skyblock.chocolatefactory;
+
+import com.mojang.brigadier.Message;
+import de.hysky.skyblocker.SkyblockerMod;
+import de.hysky.skyblocker.events.SkyblockEvents;
+import de.hysky.skyblocker.utils.Constants;
+import de.hysky.skyblocker.utils.Utils;
+import de.hysky.skyblocker.utils.scheduler.Scheduler;
+import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.regex.Pattern;
+import java.util.stream.Stream;
+
+public class TimeTowerReminder {
+ private static final String TIME_TOWER_FILE = "time_tower.txt";
+ private static final Pattern TIME_TOWER_PATTERN = Pattern.compile("^TIME TOWER! Your Chocolate Factory production has increased by \\+[\\d.]+x for \\dh!$");
+ private static final Logger LOGGER = LoggerFactory.getLogger("Skyblocker Time Tower Reminder");
+ private static boolean scheduled = false;
+
+ private TimeTowerReminder() {
+ }
+
+ public static void init() {
+ SkyblockEvents.JOIN.register(TimeTowerReminder::checkTempFile);
+ ClientReceiveMessageEvents.GAME.register(TimeTowerReminder::checkIfTimeTower);
+ }
+
+ public static void checkIfTimeTower(Message message, boolean overlay) {
+ if (!TIME_TOWER_PATTERN.matcher(message.getString()).matches() || scheduled) return;
+ Scheduler.INSTANCE.schedule(TimeTowerReminder::sendMessage, 60 * 60 * 20); // 1 hour
+ scheduled = true;
+ File tempFile = SkyblockerMod.CONFIG_DIR.resolve(TIME_TOWER_FILE).toFile();
+ if (!tempFile.exists()) {
+ try {
+ tempFile.createNewFile();
+ } catch (IOException e) {
+ LOGGER.error("[Skyblocker Time Tower Reminder] Failed to create temp file for Time Tower Reminder!", e);
+ return;
+ }
+ }
+
+ try (FileWriter writer = new FileWriter(tempFile)) {
+ writer.write(String.valueOf(System.currentTimeMillis()));
+ } catch (IOException e) {
+ LOGGER.error("[Skyblocker Time Tower Reminder] Failed to write to temp file for Time Tower Reminder!", e);
+ }
+ }
+
+ private static void sendMessage() {
+ if (MinecraftClient.getInstance().player == null || !Utils.isOnSkyblock()) return;
+ MinecraftClient.getInstance().player.sendMessage(Constants.PREFIX.get().append(Text.literal("Your Chocolate Factory's Time Tower has deactivated!").formatted(Formatting.RED)));
+
+ File tempFile = SkyblockerMod.CONFIG_DIR.resolve(TIME_TOWER_FILE).toFile();
+ try {
+ scheduled = false;
+ if (tempFile.exists()) Files.delete(tempFile.toPath());
+ } catch (Exception e) {
+ LOGGER.error("[Skyblocker Time Tower Reminder] Failed to delete temp file for Time Tower Reminder!", e);
+ }
+ }
+
+ private static void checkTempFile() {
+ File tempFile = SkyblockerMod.CONFIG_DIR.resolve(TIME_TOWER_FILE).toFile();
+ if (!tempFile.exists() || scheduled) return;
+
+ long time;
+ try (Stream<String> file = Files.lines(tempFile.toPath())) {
+ time = Long.parseLong(file.findFirst().orElseThrow());
+ } catch (Exception e) {
+ LOGGER.error("[Skyblocker Time Tower Reminder] Failed to read temp file for Time Tower Reminder!", e);
+ return;
+ }
+
+ if (System.currentTimeMillis() - time >= 60 * 60 * 1000) sendMessage();
+ else Scheduler.INSTANCE.schedule(TimeTowerReminder::sendMessage, 60 * 60 * 20 - (int) ((System.currentTimeMillis() - time) / 50)); // 50 milliseconds is 1 tick
+ }
+}