aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/de/hysky/skyblocker
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
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')
-rw-r--r--src/main/java/de/hysky/skyblocker/SkyblockerMod.java13
-rw-r--r--src/main/java/de/hysky/skyblocker/config/categories/HelperCategory.java49
-rw-r--r--src/main/java/de/hysky/skyblocker/config/configs/HelperConfig.java21
-rw-r--r--src/main/java/de/hysky/skyblocker/debug/Debug.java4
-rw-r--r--src/main/java/de/hysky/skyblocker/mixins/ClientPlayNetworkHandlerMixin.java13
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/CompactDamage.java1
-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
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ItemTooltip.java15
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/ColorUtils.java16
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/ItemUtils.java7
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/RegexUtils.java55
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/SkyblockTime.java61
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/render/gui/ContainerSolverManager.java4
15 files changed, 843 insertions, 18 deletions
diff --git a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java
index e6b2d25a..a57b4177 100644
--- a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java
+++ b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java
@@ -2,15 +2,16 @@ package de.hysky.skyblocker;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
-
-import de.hysky.skyblocker.config.datafixer.ConfigDataFixer;
import de.hysky.skyblocker.config.ImageRepoLoader;
import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.config.datafixer.ConfigDataFixer;
import de.hysky.skyblocker.debug.Debug;
import de.hysky.skyblocker.skyblock.*;
import de.hysky.skyblocker.skyblock.calculators.CalculatorCommand;
import de.hysky.skyblocker.skyblock.chat.ChatRuleAnnouncementScreen;
import de.hysky.skyblocker.skyblock.chat.ChatRulesHandler;
+import de.hysky.skyblocker.skyblock.chocolatefactory.EggFinder;
+import de.hysky.skyblocker.skyblock.chocolatefactory.TimeTowerReminder;
import de.hysky.skyblocker.skyblock.crimson.kuudra.Kuudra;
import de.hysky.skyblocker.skyblock.dungeon.*;
import de.hysky.skyblocker.skyblock.dungeon.partyfinder.PartyFinderScreen;
@@ -45,10 +46,7 @@ import de.hysky.skyblocker.skyblock.waypoint.FairySouls;
import de.hysky.skyblocker.skyblock.waypoint.MythologicalRitual;
import de.hysky.skyblocker.skyblock.waypoint.OrderedWaypoints;
import de.hysky.skyblocker.skyblock.waypoint.Relics;
-import de.hysky.skyblocker.utils.ApiUtils;
-import de.hysky.skyblocker.utils.NEURepoManager;
-import de.hysky.skyblocker.utils.ProfileUtils;
-import de.hysky.skyblocker.utils.Utils;
+import de.hysky.skyblocker.utils.*;
import de.hysky.skyblocker.utils.chat.ChatMessageListener;
import de.hysky.skyblocker.utils.discord.DiscordRPCManager;
import de.hysky.skyblocker.utils.render.RenderHelper;
@@ -183,6 +181,9 @@ public class SkyblockerMod implements ClientModInitializer {
BeaconHighlighter.init();
WarpAutocomplete.init();
MobBoundingBoxes.init();
+ EggFinder.init();
+ TimeTowerReminder.init();
+ SkyblockTime.init();
Scheduler.INSTANCE.scheduleCyclic(Utils::update, 20);
Scheduler.INSTANCE.scheduleCyclic(DiscordRPCManager::updateDataAndPresence, 200);
diff --git a/src/main/java/de/hysky/skyblocker/config/categories/HelperCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/HelperCategory.java
index 1528f853..9e4935cb 100644
--- a/src/main/java/de/hysky/skyblocker/config/categories/HelperCategory.java
+++ b/src/main/java/de/hysky/skyblocker/config/categories/HelperCategory.java
@@ -2,10 +2,11 @@ package de.hysky.skyblocker.config.categories;
import de.hysky.skyblocker.config.ConfigUtils;
import de.hysky.skyblocker.config.SkyblockerConfig;
-import dev.isxander.yacl3.api.*;
+import de.hysky.skyblocker.utils.waypoint.Waypoint;
import dev.isxander.yacl3.api.ConfigCategory;
import dev.isxander.yacl3.api.Option;
import dev.isxander.yacl3.api.OptionDescription;
+import dev.isxander.yacl3.api.OptionGroup;
import dev.isxander.yacl3.api.controller.FloatFieldControllerBuilder;
import net.minecraft.text.Text;
@@ -137,6 +138,52 @@ public class HelperCategory {
.build())
.build())
+ //Chocolate Factory
+ .group(OptionGroup.createBuilder()
+ .name(Text.translatable("skyblocker.config.helpers.chocolateFactory"))
+ .collapsed(true)
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("skyblocker.config.helpers.chocolateFactory.enableChocolateFactoryHelper"))
+ .description(OptionDescription.of(Text.translatable("skyblocker.config.helpers.chocolateFactory.enableChocolateFactoryHelper.@Tooltip")))
+ .binding(defaults.helpers.chocolateFactory.enableChocolateFactoryHelper,
+ () -> config.helpers.chocolateFactory.enableChocolateFactoryHelper,
+ newValue -> config.helpers.chocolateFactory.enableChocolateFactoryHelper = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("skyblocker.config.helpers.chocolateFactory.enableEggFinder"))
+ .description(OptionDescription.of(Text.translatable("skyblocker.config.helpers.chocolateFactory.enableEggFinder.@Tooltip")))
+ .binding(defaults.helpers.chocolateFactory.enableEggFinder,
+ () -> config.helpers.chocolateFactory.enableEggFinder,
+ newValue -> config.helpers.chocolateFactory.enableEggFinder = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("skyblocker.config.helpers.chocolateFactory.sendEggFoundMessages"))
+ .description(OptionDescription.of(Text.translatable("skyblocker.config.helpers.chocolateFactory.sendEggFoundMessages.@Tooltip")))
+ .binding(defaults.helpers.chocolateFactory.sendEggFoundMessages,
+ () -> config.helpers.chocolateFactory.sendEggFoundMessages,
+ newValue -> config.helpers.chocolateFactory.sendEggFoundMessages = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Waypoint.Type>createBuilder()
+ .name(Text.translatable("skyblocker.config.helpers.chocolateFactory.waypointType"))
+ .description(OptionDescription.of(Text.translatable("skyblocker.config.helpers.chocolateFactory.waypointType.@Tooltip")))
+ .binding(defaults.helpers.chocolateFactory.waypointType,
+ () -> config.helpers.chocolateFactory.waypointType,
+ newValue -> config.helpers.chocolateFactory.waypointType = newValue)
+ .controller(ConfigUtils::createEnumCyclingListController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("skyblocker.config.helpers.chocolateFactory.enableTimeTowerReminder"))
+ .description(OptionDescription.of(Text.translatable("skyblocker.config.helpers.chocolateFactory.enableTimeTowerReminder.@Tooltip")))
+ .binding(defaults.helpers.chocolateFactory.enableTimeTowerReminder,
+ () -> config.helpers.chocolateFactory.enableTimeTowerReminder,
+ newValue -> config.helpers.chocolateFactory.enableTimeTowerReminder = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .build())
+
.build();
}
}
diff --git a/src/main/java/de/hysky/skyblocker/config/configs/HelperConfig.java b/src/main/java/de/hysky/skyblocker/config/configs/HelperConfig.java
index 2abff6ac..c0314924 100644
--- a/src/main/java/de/hysky/skyblocker/config/configs/HelperConfig.java
+++ b/src/main/java/de/hysky/skyblocker/config/configs/HelperConfig.java
@@ -1,5 +1,6 @@
package de.hysky.skyblocker.config.configs;
+import de.hysky.skyblocker.utils.waypoint.Waypoint;
import dev.isxander.yacl3.config.v2.api.SerialEntry;
public class HelperConfig {
@@ -19,6 +20,9 @@ public class HelperConfig {
@SerialEntry
public FairySouls fairySouls = new FairySouls();
+ @SerialEntry
+ public ChocolateFactory chocolateFactory = new ChocolateFactory();
+
public static class MythologicalRitual {
@SerialEntry
public boolean enableMythologicalRitualHelper = true;
@@ -62,4 +66,21 @@ public class HelperConfig {
@SerialEntry
public boolean highlightOnlyNearbySouls = false;
}
+
+ public static class ChocolateFactory {
+ @SerialEntry
+ public boolean enableChocolateFactoryHelper = true;
+
+ @SerialEntry
+ public boolean enableEggFinder = true;
+
+ @SerialEntry
+ public boolean sendEggFoundMessages = true;
+
+ @SerialEntry
+ public Waypoint.Type waypointType = Waypoint.Type.WAYPOINT;
+
+ @SerialEntry
+ public boolean enableTimeTowerReminder = true;
+ }
}
diff --git a/src/main/java/de/hysky/skyblocker/debug/Debug.java b/src/main/java/de/hysky/skyblocker/debug/Debug.java
index d9ac668c..d642ca5b 100644
--- a/src/main/java/de/hysky/skyblocker/debug/Debug.java
+++ b/src/main/java/de/hysky/skyblocker/debug/Debug.java
@@ -83,9 +83,7 @@ public class Debug {
Iterable<ItemStack> equippedItems = armorStand.getEquippedItems();
for (ItemStack stack : equippedItems) {
- String texture = ItemUtils.getHeadTexture(stack);
-
- if (!texture.isEmpty()) context.getSource().sendFeedback(Text.of(texture));
+ ItemUtils.getHeadTextureOptional(stack).ifPresent(texture -> context.getSource().sendFeedback(Text.of(texture)));
}
}
diff --git a/src/main/java/de/hysky/skyblocker/mixins/ClientPlayNetworkHandlerMixin.java b/src/main/java/de/hysky/skyblocker/mixins/ClientPlayNetworkHandlerMixin.java
index 2c2c1376..48389d40 100644
--- a/src/main/java/de/hysky/skyblocker/mixins/ClientPlayNetworkHandlerMixin.java
+++ b/src/main/java/de/hysky/skyblocker/mixins/ClientPlayNetworkHandlerMixin.java
@@ -3,9 +3,9 @@ package de.hysky.skyblocker.mixins;
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
import com.llamalad7.mixinextras.injector.v2.WrapWithCondition;
import com.llamalad7.mixinextras.sugar.Local;
-import de.hysky.skyblocker.config.SkyblockerConfigManager;
import de.hysky.skyblocker.skyblock.CompactDamage;
import de.hysky.skyblocker.skyblock.FishingHelper;
+import de.hysky.skyblocker.skyblock.chocolatefactory.EggFinder;
import de.hysky.skyblocker.skyblock.dungeon.DungeonScore;
import de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonManager;
import de.hysky.skyblocker.skyblock.end.BeaconHighlighter;
@@ -84,7 +84,7 @@ public abstract class ClientPlayNetworkHandlerMixin {
return !(Utils.isOnHypixel() && ((Identifier) identifier).getNamespace().equals("badlion"));
}
- @WrapWithCondition(method = { "onScoreboardScoreUpdate", "onScoreboardScoreReset" }, at = @At(value = "INVOKE", target = "Lorg/slf4j/Logger;warn(Ljava/lang/String;Ljava/lang/Object;)V", remap = false))
+ @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();
}
@@ -111,11 +111,18 @@ public abstract class ClientPlayNetworkHandlerMixin {
@Inject(method = "onEntityTrackerUpdate", at = @At("TAIL"))
private void skyblocker$onEntityTrackerUpdate(EntityTrackerUpdateS2CPacket packet, CallbackInfo ci, @Local Entity entity) {
- if (!SkyblockerConfigManager.get().uiAndVisuals.compactDamage.enabled || !(entity instanceof ArmorStandEntity armorStandEntity)) return;
+ if (!(entity instanceof ArmorStandEntity armorStandEntity)) return;
+
+ EggFinder.checkIfEgg(armorStandEntity);
try { //Prevent packet handling fails if something goes wrong so that entity trackers still update, just without compact damage numbers
CompactDamage.compactDamage(armorStandEntity);
} catch (Exception e) {
LOGGER.error("[Skyblocker Compact Damage] Failed to compact damage number", e);
}
}
+
+ @Inject(method = "onEntityEquipmentUpdate", at = @At(value = "TAIL"))
+ private void skyblocker$onEntityEquip(EntityEquipmentUpdateS2CPacket packet, CallbackInfo ci, @Local Entity entity) {
+ EggFinder.checkIfEgg(entity);
+ }
}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/CompactDamage.java b/src/main/java/de/hysky/skyblocker/skyblock/CompactDamage.java
index 2837364b..8285a823 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/CompactDamage.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/CompactDamage.java
@@ -19,6 +19,7 @@ public class CompactDamage {
}
public static void compactDamage(ArmorStandEntity entity) {
+ if (!SkyblockerConfigManager.get().uiAndVisuals.compactDamage.enabled) return;
if (!entity.isInvisible() || !entity.hasCustomName() || !entity.isCustomNameVisible()) return;
Text customName = entity.getCustomName();
String customNameStringified = customName.getString();
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
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ItemTooltip.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ItemTooltip.java
index c6caaf41..8c083e25 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ItemTooltip.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ItemTooltip.java
@@ -2,7 +2,6 @@ package de.hysky.skyblocker.skyblock.item.tooltip;
import com.google.gson.JsonObject;
import de.hysky.skyblocker.SkyblockerMod;
-import de.hysky.skyblocker.config.SkyblockerConfig;
import de.hysky.skyblocker.config.SkyblockerConfigManager;
import de.hysky.skyblocker.config.configs.GeneralConfig;
import de.hysky.skyblocker.skyblock.item.MuseumItemCache;
@@ -394,15 +393,23 @@ public class ItemTooltip {
return message;
}
+ //This is static to not create a new text object for each line in every item
+ private static final Text BUMPY_LINE = Text.literal("-----------------").formatted(Formatting.DARK_GRAY, Formatting.STRIKETHROUGH);
+
private static void smoothenLines(List<Text> lines) {
for (int i = 0; i < lines.size(); i++) {
- Text line = lines.get(i);
- if (line.getString().equals("-----------------")) {
- lines.set(i, Text.literal(" ").formatted(Formatting.DARK_GRAY, Formatting.STRIKETHROUGH, Formatting.BOLD));
+ List<Text> lineSiblings = lines.get(i).getSiblings();
+ //Compare the first sibling rather than the whole object as the style of the root object can change while visually staying the same
+ if (lineSiblings.size() == 1 && lineSiblings.getFirst().equals(BUMPY_LINE)) {
+ lines.set(i, createSmoothLine());
}
}
}
+ public static Text createSmoothLine() {
+ return Text.literal(" ").formatted(Formatting.DARK_GRAY, Formatting.STRIKETHROUGH, Formatting.BOLD);
+ }
+
// If these options is true beforehand, the client will get first data of these options while loading.
// After then, it will only fetch the data if it is on Skyblock.
public static int minute = 0;
diff --git a/src/main/java/de/hysky/skyblocker/utils/ColorUtils.java b/src/main/java/de/hysky/skyblocker/utils/ColorUtils.java
new file mode 100644
index 00000000..0196edf2
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/utils/ColorUtils.java
@@ -0,0 +1,16 @@
+package de.hysky.skyblocker.utils;
+
+public class ColorUtils {
+ /**
+ * Takes an RGB color as an integer and returns an array of the color's components as floats, in RGB format.
+ * @param color The color to get the components of.
+ * @return An array of the color's components as floats.
+ */
+ public static float[] getFloatComponents(int color) {
+ return new float[] {
+ ((color >> 16) & 0xFF) / 255f,
+ ((color >> 8) & 0xFF) / 255f,
+ (color & 0xFF) / 255f
+ };
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/utils/ItemUtils.java b/src/main/java/de/hysky/skyblocker/utils/ItemUtils.java
index 1aa77080..13b28808 100644
--- a/src/main/java/de/hysky/skyblocker/utils/ItemUtils.java
+++ b/src/main/java/de/hysky/skyblocker/utils/ItemUtils.java
@@ -33,6 +33,7 @@ import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAccessor;
+import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
@@ -214,6 +215,12 @@ public class ItemUtils {
return texture;
}
+ public static Optional<String> getHeadTextureOptional(ItemStack stack) {
+ String texture = getHeadTexture(stack);
+ if (texture.isBlank()) return Optional.empty();
+ return Optional.of(texture);
+ }
+
public static ItemStack getSkyblockerStack() {
try {
ItemStack stack = new ItemStack(Items.PLAYER_HEAD);
diff --git a/src/main/java/de/hysky/skyblocker/utils/RegexUtils.java b/src/main/java/de/hysky/skyblocker/utils/RegexUtils.java
new file mode 100644
index 00000000..5b91a80b
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/utils/RegexUtils.java
@@ -0,0 +1,55 @@
+package de.hysky.skyblocker.utils;
+
+import java.util.OptionalDouble;
+import java.util.OptionalInt;
+import java.util.OptionalLong;
+import java.util.regex.Matcher;
+
+public class RegexUtils {
+ /**
+ * @return An OptionalLong of the first group in the matcher, or an empty OptionalLong if the matcher doesn't find anything.
+ */
+ public static OptionalLong getLongFromMatcher(Matcher matcher) {
+ return getLongFromMatcher(matcher, matcher.hasMatch() ? matcher.end() : 0);
+ }
+
+ /**
+ * @return An OptionalLong of the first group in the matcher, or an empty OptionalLong if the matcher doesn't find anything.
+ */
+ public static OptionalLong getLongFromMatcher(Matcher matcher, int startingIndex) {
+ if (!matcher.find(startingIndex)) return OptionalLong.empty();
+ return OptionalLong.of(Long.parseLong(matcher.group(1).replace(",", "")));
+ }
+
+ /**
+ * @return An OptionalInt of the first group in the matcher, or an empty OptionalInt if the matcher doesn't find anything.
+ */
+ public static OptionalInt getIntFromMatcher(Matcher matcher) {
+ return getIntFromMatcher(matcher, matcher.hasMatch() ? matcher.end() : 0);
+ }
+
+ /**
+ * @return An OptionalInt of the first group in the matcher, or an empty OptionalInt if the matcher doesn't find anything.
+ */
+ public static OptionalInt getIntFromMatcher(Matcher matcher, int startingIndex) {
+ if (!matcher.find(startingIndex)) return OptionalInt.empty();
+ return OptionalInt.of(Integer.parseInt(matcher.group(1).replace(",", "")));
+ }
+
+ /**
+ * @return An OptionalDouble of the first group in the matcher, or an empty OptionalDouble if the matcher doesn't find anything.
+ * @implNote Assumes the decimal separator is `.`
+ */
+ public static OptionalDouble getDoubleFromMatcher(Matcher matcher) {
+ return getDoubleFromMatcher(matcher, matcher.hasMatch() ? matcher.end() : 0);
+ }
+
+ /**
+ * @return An OptionalDouble of the first group in the matcher, or an empty OptionalDouble if the matcher doesn't find anything.
+ * @implNote Assumes the decimal separator is `.`
+ */
+ public static OptionalDouble getDoubleFromMatcher(Matcher matcher, int startingIndex) {
+ if (!matcher.find(startingIndex)) return OptionalDouble.empty();
+ return OptionalDouble.of(Double.parseDouble(matcher.group(1).replace(",", "")));
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/utils/SkyblockTime.java b/src/main/java/de/hysky/skyblocker/utils/SkyblockTime.java
new file mode 100644
index 00000000..045ecc4e
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/utils/SkyblockTime.java
@@ -0,0 +1,61 @@
+package de.hysky.skyblocker.utils;
+
+import de.hysky.skyblocker.utils.scheduler.Scheduler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+public class SkyblockTime {
+ private static final long SKYBLOCK_EPOCH = 1560275700000L;
+ public static final AtomicInteger skyblockYear = new AtomicInteger(0);
+ public static final AtomicReference<Season> skyblockSeason = new AtomicReference<>(Season.SPRING);
+ public static final AtomicReference<Month> skyblockMonth = new AtomicReference<>(Month.EARLY_SPRING);
+ public static final AtomicInteger skyblockDay = new AtomicInteger(0);
+ private static final Logger LOGGER = LoggerFactory.getLogger("Skyblocker Time");
+
+ private SkyblockTime() {
+ }
+
+ public static void init() {
+ updateTime();
+ //ScheduleCyclic already runs the task upon scheduling, so there's no need to call updateTime() here
+ Scheduler.INSTANCE.schedule(() -> Scheduler.INSTANCE.scheduleCyclic(SkyblockTime::updateTime, 1200 * 24), (int) (1200000 - (getSkyblockMillis() % 1200000)) / 50);
+ }
+
+ private static long getSkyblockMillis() {
+ return System.currentTimeMillis() - SKYBLOCK_EPOCH;
+ }
+
+ private static int getSkyblockYear() {
+ return (int) (Math.floor(getSkyblockMillis() / 446400000.0) + 1);
+ }
+
+ private static int getSkyblockMonth() {
+ return (int) (Math.floor(getSkyblockMillis() / 37200000.0) % 12);
+ }
+
+ private static int getSkyblockDay() {
+ return (int) (Math.floor(getSkyblockMillis() / 1200000.0) % 31 + 1);
+ }
+
+ private static void updateTime() {
+ skyblockYear.set(getSkyblockYear());
+ skyblockSeason.set(Season.values()[getSkyblockMonth() / 3]);
+ skyblockMonth.set(Month.values()[getSkyblockMonth()]);
+ skyblockDay.set(getSkyblockDay());
+ LOGGER.info("[Skyblocker Time] Skyblock time updated to Year {}, Season {}, Month {}, Day {}", skyblockYear.get(), skyblockSeason.get(), skyblockMonth.get(), skyblockDay.get());
+ }
+
+ public enum Season {
+ SPRING, SUMMER, FALL, WINTER
+ }
+
+ public enum Month {
+ EARLY_SPRING, SPRING, LATE_SPRING,
+ EARLY_SUMMER, SUMMER, LATE_SUMMER,
+ EARLY_FALL, FALL, LATE_FALL,
+ EARLY_WINTER, WINTER, LATE_WINTER
+ }
+}
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
index 08fb6a86..8a5d32be 100644
--- a/src/main/java/de/hysky/skyblocker/utils/render/gui/ContainerSolverManager.java
+++ b/src/main/java/de/hysky/skyblocker/utils/render/gui/ContainerSolverManager.java
@@ -5,6 +5,7 @@ import com.mojang.blaze3d.systems.RenderSystem;
import de.hysky.skyblocker.mixins.accessors.HandledScreenAccessor;
import de.hysky.skyblocker.skyblock.accessories.newyearcakes.NewYearCakeBagHelper;
import de.hysky.skyblocker.skyblock.accessories.newyearcakes.NewYearCakesHelper;
+import de.hysky.skyblocker.skyblock.chocolatefactory.ChocolateFactorySolver;
import de.hysky.skyblocker.skyblock.dungeon.CroesusHelper;
import de.hysky.skyblocker.skyblock.dungeon.CroesusProfit;
import de.hysky.skyblocker.skyblock.dungeon.terminal.ColorTerminal;
@@ -55,7 +56,8 @@ public class ContainerSolverManager {
new SuperpairsSolver(),
UltrasequencerSolver.INSTANCE,
new NewYearCakeBagHelper(),
- NewYearCakesHelper.INSTANCE
+ NewYearCakesHelper.INSTANCE,
+ new ChocolateFactorySolver()
};
}