aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/de/hysky/skyblocker/skyblock
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/de/hysky/skyblocker/skyblock')
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/CompactDamage.java1
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/auction/widgets/CategoryTabWidget.java28
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/chat/ChatRule.java14
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/chat/ChatRuleConfigScreen.java1
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/chat/ChatRulesConfigListWidget.java2
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/chat/ChatRulesHandler.java47
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/ChocolateFactorySolver.java383
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/EggFinder.java179
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/TimeTowerReminder.java88
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dwarven/GlaciteColdOverlay.java70
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/events/EventNotifications.java174
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/events/EventToast.java93
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/events/JacobEventToast.java60
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/garden/VisitorHelper.java52
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ItemTooltip.java40
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemListTab.java89
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemListWidget.java136
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/itemlist/SearchResultsWidget.java15
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/itemlist/UpcomingEventsTab.java168
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/searchoverlay/OverlayScreen.java103
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/searchoverlay/SearchOverManager.java116
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/JacobsContestWidget.java2
22 files changed, 1698 insertions, 163 deletions
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/auction/widgets/CategoryTabWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/auction/widgets/CategoryTabWidget.java
index 02dbc132..a0b5f0b9 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/auction/widgets/CategoryTabWidget.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/auction/widgets/CategoryTabWidget.java
@@ -1,42 +1,26 @@
package de.hysky.skyblocker.skyblock.auction.widgets;
import de.hysky.skyblocker.skyblock.auction.SlotClickHandler;
+import de.hysky.skyblocker.utils.render.gui.SideTabButtonWidget;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.DrawContext;
-import net.minecraft.client.gui.screen.ButtonTextures;
-import net.minecraft.client.gui.widget.ToggleButtonWidget;
import net.minecraft.client.item.TooltipType;
import net.minecraft.item.Item.TooltipContext;
import net.minecraft.item.ItemStack;
-import net.minecraft.util.Identifier;
import org.jetbrains.annotations.NotNull;
-public class CategoryTabWidget extends ToggleButtonWidget {
- private static final ButtonTextures TEXTURES = new ButtonTextures(new Identifier("recipe_book/tab"), new Identifier("recipe_book/tab_selected"));
-
- public void setIcon(@NotNull ItemStack icon) {
- this.icon = icon.copy();
- }
-
- private @NotNull ItemStack icon;
+public class CategoryTabWidget extends SideTabButtonWidget {
private final SlotClickHandler slotClick;
private int slotId = -1;
public CategoryTabWidget(@NotNull ItemStack icon, SlotClickHandler slotClick) {
- super(0, 0, 35, 27, false);
- this.icon = icon.copy(); // copy prevents item disappearing on click
+ super(0, 0, false, icon);
this.slotClick = slotClick;
- setTextures(TEXTURES);
}
@Override
public void renderWidget(DrawContext context, int mouseX, int mouseY, float delta) {
- if (textures == null) return;
- Identifier identifier = textures.get(true, this.toggled);
- int x = getX();
- if (toggled) x -= 2;
- context.drawGuiTexture(identifier, x, this.getY(), this.width, this.height);
- context.drawItem(icon, x + 9, getY() + 5);
+ super.renderWidget(context, mouseX, mouseY, delta);
if (isMouseOver(mouseX, mouseY)) {
context.getMatrices().push();
@@ -52,8 +36,8 @@ public class CategoryTabWidget extends ToggleButtonWidget {
@Override
public void onClick(double mouseX, double mouseY) {
- if (this.toggled || slotId == -1) return;
+ if (isToggled() || slotId == -1) return;
+ super.onClick(mouseX, mouseY);
slotClick.click(slotId);
- this.setToggled(true);
}
}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/chat/ChatRule.java b/src/main/java/de/hysky/skyblocker/skyblock/chat/ChatRule.java
index 34cc6352..7fd6844d 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/chat/ChatRule.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/chat/ChatRule.java
@@ -232,19 +232,19 @@ public class ChatRule {
return true;
}
- String rawLocation = Utils.getLocationRaw();
+ String cleanedMapLocation = Utils.getMap().toLowerCase().replace(" ", "");
Boolean isLocationValid = null;
-
- for (String validLocation : validLocations.replace(" ", "").toLowerCase().split(",")) {//the locations are raw locations split by "," and start with ! if not locations
- String rawValidLocation = ChatRulesHandler.locations.get(validLocation.replace("!",""));
- if (rawValidLocation == null) continue;
+ for (String validLocation : validLocations.replace(" ", "").toLowerCase().split(",")) {//the locations are split by "," and start with ! if not locations
+ if (validLocation == null) continue;
if (validLocation.startsWith("!")) {//not location
- if (Objects.equals(rawValidLocation, rawLocation.toLowerCase())) {
+ if (Objects.equals(validLocation.substring(1), cleanedMapLocation)) {
isLocationValid = false;
break;
+ } else {
+ isLocationValid = true;
}
} else {
- if (Objects.equals(rawValidLocation, rawLocation.toLowerCase())) { //normal location
+ if (Objects.equals(validLocation, cleanedMapLocation)) { //normal location
isLocationValid = true;
break;
}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/chat/ChatRuleConfigScreen.java b/src/main/java/de/hysky/skyblocker/skyblock/chat/ChatRuleConfigScreen.java
index cb6e8cc8..9ecb71e2 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/chat/ChatRuleConfigScreen.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/chat/ChatRuleConfigScreen.java
@@ -156,6 +156,7 @@ public class ChatRuleConfigScreen extends Screen {
locationLabelTextPos = currentPos;
lineXOffset = client.textRenderer.getWidth(Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.locations")) + SPACER_X;
locationsInput = new TextFieldWidget(client.textRenderer, currentPos.leftInt() + lineXOffset, currentPos.rightInt(), 200, 20, Text.of(""));
+ locationsInput.setMaxLength(96);
locationsInput.setText(chatRule.getValidLocations());
MutableText locationToolTip = Text.translatable("skyblocker.config.chat.chatRules.screen.ruleScreen.locations.@Tooltip");
locationToolTip.append("\n");
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/chat/ChatRulesConfigListWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/chat/ChatRulesConfigListWidget.java
index 29c052b8..1fb763e2 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/chat/ChatRulesConfigListWidget.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/chat/ChatRulesConfigListWidget.java
@@ -45,7 +45,7 @@ public class ChatRulesConfigListWidget extends ElementListWidget<ChatRulesConfig
protected void addRuleAfterSelected() {
hasChanged = true;
- int newIndex = children().indexOf(getSelectedOrNull()) + 1;
+ int newIndex = Math.max(children().indexOf(getSelectedOrNull()), 0);
ChatRulesHandler.chatRuleList.add(newIndex, new ChatRule());
children().add(newIndex + 1, new ChatRuleConfigEntry(newIndex));
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/chat/ChatRulesHandler.java b/src/main/java/de/hysky/skyblocker/skyblock/chat/ChatRulesHandler.java
index d1c7f4fd..90a3b641 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/chat/ChatRulesHandler.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/chat/ChatRulesHandler.java
@@ -8,6 +8,7 @@ import com.mojang.serialization.JsonOps;
import de.hysky.skyblocker.SkyblockerMod;
import de.hysky.skyblocker.mixins.accessors.MessageHandlerAccessor;
import de.hysky.skyblocker.utils.Http;
+import de.hysky.skyblocker.utils.Location;
import de.hysky.skyblocker.utils.Utils;
import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents;
import net.minecraft.client.MinecraftClient;
@@ -34,19 +35,33 @@ public class ChatRulesHandler {
private static final Path CHAT_RULE_FILE = SkyblockerMod.CONFIG_DIR.resolve("chat_rules.json");
private static final Codec<Map<String, List<ChatRule>>> MAP_CODEC = Codec.unboundedMap(Codec.STRING, ChatRule.LIST_CODEC);
/**
- * look up table for the locations input by the users to raw locations
- */
- protected static final HashMap<String, String> locations = new HashMap<>();
- /**
* list of possible locations still formatted for the tool tip
*/
- protected static final List<String> locationsList = new ArrayList<>();
+ protected static final List<String> locationsList = List.of (
+ "The Farming Islands",
+ "Crystal Hollows",
+ "Jerry's Workshop",
+ "The Park",
+ "Dark Auction",
+ "Dungeons",
+ "The End",
+ "Crimson Isle",
+ "Hub",
+ "Kuudra's Hollow",
+ "Private Island",
+ "Dwarven Mines",
+ "The Garden",
+ "Gold Mine",
+ "Blazing Fortress",
+ "Deep Caverns",
+ "Spider's Den",
+ "Mineshaft"
+ );
protected static final List<ChatRule> chatRuleList = new ArrayList<>();
public static void init() {
CompletableFuture.runAsync(ChatRulesHandler::loadChatRules);
- CompletableFuture.runAsync(ChatRulesHandler::loadLocations);
ClientReceiveMessageEvents.ALLOW_GAME.register(ChatRulesHandler::checkMessage);
}
@@ -76,26 +91,6 @@ public class ChatRulesHandler {
chatRuleList.add(miningAbilityRule);
}
- private static void loadLocations() {
- try {
- String response = Http.sendGetRequest("https://api.hypixel.net/v2/resources/games");
- JsonObject locationsJson = JsonParser.parseString(response).getAsJsonObject().get("games").getAsJsonObject().get("SKYBLOCK").getAsJsonObject().get("modeNames").getAsJsonObject();
- for (Map.Entry<String, JsonElement> entry : locationsJson.entrySet()) {
- //fix old naming todo remove when hypixel fix
- if (Objects.equals(entry.getKey(), "instanced")) {
- locationsList.add(entry.getValue().getAsString());
- locations.put(entry.getValue().getAsString().replace(" ", "").toLowerCase(), "kuudra");
- continue;
- }
- locationsList.add(entry.getValue().getAsString());
- //add to list in a simplified for so more lenient for user input
- locations.put(entry.getValue().getAsString().replace(" ", "").toLowerCase(), entry.getKey());
- }
- } catch (Exception e) {
- LOGGER.error("[Skyblocker Chat Rules] Failed to load locations!", e);
- }
- }
-
protected static void saveChatRules() {
JsonObject chatRuleJson = new JsonObject();
chatRuleJson.add("rules", ChatRule.LIST_CODEC.encodeStart(JsonOps.INSTANCE, chatRuleList).getOrThrow());
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..e04e632a
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/ChocolateFactorySolver.java
@@ -0,0 +1,383 @@
+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 {
+ //Patterns
+ 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<>(8);
+ private static long totalChocolate = -1L;
+ private static double totalCps = -1.0;
+ private static double totalCpsMultiplier = -1.0;
+ private static long requiredUntilNextPrestige = -1L;
+ private static boolean canPrestige = false;
+ private static boolean reachedMaxPrestige = false;
+ 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;
+
+ //Slots, for ease of maintenance rather than using magic numbers everywhere.
+ private static final byte RABBITS_START = 28;
+ private static final byte RABBITS_END = 34;
+ private static final byte COACH_SLOT = 42;
+ private static final byte CHOCOLATE_SLOT = 13;
+ private static final byte CPS_SLOT = 45;
+ private static final byte PRESTIGE_SLOT = 27;
+ private static final byte TIME_TOWER_SLOT = 39;
+ private static final byte STRAY_RABBIT_START = 0;
+ private static final byte STRAY_RABBIT_END = 26;
+
+ 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().ifPresent(highlights::add);
+ highlights.addAll(getStrayRabbitHighlight(slots));
+
+ 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 = RABBITS_START; i <= RABBITS_END; i++) { // The 7 rabbits slots are in 28, 29, 30, 31, 32, 33 and 34.
+ 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(COACH_SLOT)).ifPresent(cpsIncreaseFactors::add);
+
+ //The clickable chocolate is in slot 13, holds the total chocolate
+ RegexUtils.getLongFromMatcher(CHOCOLATE_PATTERN.matcher(slots.get(CHOCOLATE_SLOT).getName().getString())).ifPresent(l -> totalChocolate = l);
+
+ //Cps item (cocoa bean) is in slot 45
+ String cpsItemLore = getConcatenatedLore(slots.get(CPS_SLOT));
+ 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
+ String prestigeLore = getConcatenatedLore(slots.get(PRESTIGE_SLOT));
+ Matcher prestigeMatcher = PRESTIGE_REQUIREMENT_PATTERN.matcher(prestigeLore);
+ 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();
+ }
+ } else if (prestigeLore.endsWith("Click to prestige!")) {
+ canPrestige = true;
+ reachedMaxPrestige = false;
+ } else if (prestigeLore.endsWith("You have reached max prestige!")) {
+ canPrestige = false;
+ reachedMaxPrestige = true;
+ }
+
+ //Time Tower is in slot 39
+ timeTowerMultiplier = romanToDecimal(StringUtils.substringAfterLast(slots.get(TIME_TOWER_SLOT).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(TIME_TOWER_SLOT)));
+ 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)) {
+ 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 (totalCps < 0.0 || reachedMaxPrestige) return false;
+ if (requiredUntilNextPrestige > 0 && !canPrestige) {
+ 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() //Keep this outside of the `if` to match the format of the upgrade tooltips, that say "Time until upgrade: Now" when it's possible
+ .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(), COACH_SLOT, 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() {
+ if (reachedMaxPrestige) return Optional.empty();
+ if (canPrestige) return Optional.of(ColorHighlight.green(PRESTIGE_SLOT));
+ return Optional.of(ColorHighlight.red(PRESTIGE_SLOT));
+ }
+
+ private static List<ColorHighlight> getStrayRabbitHighlight(Int2ObjectMap<ItemStack> slots) {
+ final List<ColorHighlight> highlights = new ArrayList<>();
+ for (byte i = STRAY_RABBIT_START; i <= STRAY_RABBIT_END; i++) {
+ ItemStack item = slots.get(i);
+ if (!item.isOf(Items.PLAYER_HEAD)) continue;
+ String name = item.getName().getString();
+ if (name.equals("CLICK ME!") || name.startsWith("GOLDEN RABBIT")) {
+ highlights.add(ColorHighlight.green(i));
+ }
+ }
+ return highlights;
+ }
+
+ 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..c3a76632
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/EggFinder.java
@@ -0,0 +1,179 @@
+package de.hysky.skyblocker.skyblock.chocolatefactory;
+
+import com.mojang.brigadier.Command;
+import com.mojang.brigadier.arguments.IntegerArgumentType;
+import com.mojang.brigadier.arguments.StringArgumentType;
+import de.hysky.skyblocker.SkyblockerMod;
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.events.SkyblockEvents;
+import de.hysky.skyblocker.utils.*;
+import de.hysky.skyblocker.utils.scheduler.MessageScheduler;
+import de.hysky.skyblocker.utils.waypoint.Waypoint;
+import it.unimi.dsi.fastutil.objects.ObjectImmutableList;
+import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager;
+import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback;
+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.ClickEvent;
+import net.minecraft.text.HoverEvent;
+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 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, Location.DEEP_CAVERNS, Location.SPIDERS_DEN, Location.THE_FARMING_ISLAND};
+ 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);
+ ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> dispatcher.register(ClientCommandManager.literal(SkyblockerMod.NAMESPACE)
+ .then(ClientCommandManager.literal("eggFinder")
+ .then(ClientCommandManager.literal("shareLocation")
+ .then(ClientCommandManager.argument("x", IntegerArgumentType.integer())
+ .then(ClientCommandManager.argument("y", IntegerArgumentType.integer())
+ .then(ClientCommandManager.argument("z", IntegerArgumentType.integer())
+ .then(ClientCommandManager.argument("eggType", StringArgumentType.word())
+ .executes(context -> {
+ MessageScheduler.INSTANCE.sendMessageAfterCooldown("[Skyblocker] Chocolate " + context.getArgument("eggType", String.class) + " Egg found at " + context.getArgument("x", Integer.class) + " " + context.getArgument("y", Integer.class) + " " + context.getArgument("z", Integer.class) + "!");
+ return Command.SINGLE_SUCCESS;
+ })))))))));
+ }
+
+ 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() + "!")
+ .styled(style -> style.withClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/skyblocker eggFinder shareLocation " + entity.getBlockX() + " " + entity.getBlockY() + 2 + " " + entity.getBlockZ() + " " + eggType))
+ .withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Text.literal("Click to share the location in chat!").formatted(Formatting.GREEN)))));
+ }
+
+ 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);
+ }
+ }
+ }
+
+ 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..c679f152
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/TimeTowerReminder.java
@@ -0,0 +1,88 @@
+package de.hysky.skyblocker.skyblock.chocolatefactory;
+
+import com.mojang.brigadier.Message;
+import de.hysky.skyblocker.SkyblockerMod;
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+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())); //Overwrites the file so no need to handle case where the file already exists and has text
+ } 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;
+ if (SkyblockerConfigManager.get().helpers.chocolateFactory.enableTimeTowerReminder) {
+ 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/dwarven/GlaciteColdOverlay.java b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/GlaciteColdOverlay.java
new file mode 100644
index 00000000..3839a712
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/GlaciteColdOverlay.java
@@ -0,0 +1,70 @@
+package de.hysky.skyblocker.skyblock.dwarven;
+
+import com.mojang.blaze3d.systems.RenderSystem;
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+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.gui.DrawContext;
+import net.minecraft.text.Text;
+import net.minecraft.util.Identifier;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class GlaciteColdOverlay {
+ private static final Identifier POWDER_SNOW_OUTLINE = new Identifier("textures/misc/powder_snow_outline.png");
+ private static final Pattern COLD_PATTERN = Pattern.compile("Cold: -(\\d+)❄");
+ private static int cold = 0;
+ private static long resetTime = System.currentTimeMillis();
+
+ public static void init() {
+ Scheduler.INSTANCE.scheduleCyclic(GlaciteColdOverlay::update, 20);
+ ClientReceiveMessageEvents.GAME.register(GlaciteColdOverlay::coldReset);
+ }
+
+ private static void coldReset(Text text, boolean b) {
+ if (!Utils.isInDwarvenMines() || b) {
+ return;
+ }
+ String message = text.getString();
+ if (message.equals("The warmth of the campfire reduced your ❄ Cold to 0!")) {
+ cold = 0;
+ resetTime = System.currentTimeMillis();
+ }
+ }
+
+ private static void update() {
+ if (!Utils.isInDwarvenMines() || System.currentTimeMillis() - resetTime < 3000 || !SkyblockerConfigManager.get().mining.glacite.coldOverlay) {
+ cold = 0;
+ return;
+ }
+ for (String line : Utils.STRING_SCOREBOARD) {
+ Matcher coldMatcher = COLD_PATTERN.matcher(line);
+ if (coldMatcher.matches()) {
+ String value = coldMatcher.group(1);
+ cold = Integer.parseInt(value);
+ return;
+ }
+ }
+ cold = 0;
+ }
+
+ private static void renderOverlay(DrawContext context, Identifier texture, float opacity) {
+ RenderSystem.disableDepthTest();
+ RenderSystem.depthMask(false);
+ RenderSystem.enableBlend();
+ context.setShaderColor(1.0f, 1.0f, 1.0f, opacity);
+ context.drawTexture(texture, 0, 0, -90, 0.0f, 0.0f, context.getScaledWindowWidth(), context.getScaledWindowHeight(), context.getScaledWindowWidth(), context.getScaledWindowHeight());
+ RenderSystem.disableBlend();
+ RenderSystem.depthMask(true);
+ RenderSystem.enableDepthTest();
+ context.setShaderColor(1.0f, 1.0f, 1.0f, 1.0f);
+ }
+
+ public static void render(DrawContext context) {
+ if (Utils.isInDwarvenMines() && SkyblockerConfigManager.get().mining.glacite.coldOverlay) {
+ renderOverlay(context, POWDER_SNOW_OUTLINE, cold / 100f);
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/events/EventNotifications.java b/src/main/java/de/hysky/skyblocker/skyblock/events/EventNotifications.java
new file mode 100644
index 00000000..0fd41969
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/events/EventNotifications.java
@@ -0,0 +1,174 @@
+package de.hysky.skyblocker.skyblock.events;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.mojang.brigadier.arguments.BoolArgumentType;
+import com.mojang.brigadier.arguments.IntegerArgumentType;
+import com.mojang.logging.LogUtils;
+import de.hysky.skyblocker.SkyblockerMod;
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.events.SkyblockEvents;
+import de.hysky.skyblocker.utils.Http;
+import de.hysky.skyblocker.utils.scheduler.Scheduler;
+import it.unimi.dsi.fastutil.ints.IntList;
+import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
+import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager;
+import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.sound.PositionedSoundInstance;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.Items;
+import net.minecraft.sound.SoundEvent;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+
+import java.util.*;
+import java.util.concurrent.CompletableFuture;
+
+public class EventNotifications {
+ private static final Logger LOGGER = LogUtils.getLogger();
+
+ private static long currentTime = System.currentTimeMillis() / 1000;
+
+ public static final String JACOBS = "Jacob's Farming Contest";
+
+ public static final IntList DEFAULT_REMINDERS = IntList.of(60, 60 * 5);
+
+ public static final Map<String, ItemStack> eventIcons = new Object2ObjectOpenHashMap<>();
+
+ static {
+ eventIcons.put("Dark Auction", new ItemStack(Items.NETHER_BRICK));
+ eventIcons.put("Bonus Fishing Festival", new ItemStack(Items.FISHING_ROD));
+ eventIcons.put("Bonus Mining Fiesta", new ItemStack(Items.IRON_PICKAXE));
+ eventIcons.put(JACOBS, new ItemStack(Items.IRON_HOE));
+ eventIcons.put("New Year Celebration", new ItemStack(Items.CAKE));
+ eventIcons.put("Election Over!", new ItemStack(Items.JUKEBOX));
+ eventIcons.put("Election Booth Opens", new ItemStack(Items.JUKEBOX));
+ eventIcons.put("Spooky Festival", new ItemStack(Items.JACK_O_LANTERN));
+ eventIcons.put("Season of Jerry", new ItemStack(Items.SNOWBALL));
+ eventIcons.put("Jerry's Workshop Opens", new ItemStack(Items.SNOW_BLOCK));
+ eventIcons.put("Traveling Zoo", new ItemStack(Items.HAY_BLOCK)); // change to the custom head one day
+ }
+
+ public static void init() {
+ Scheduler.INSTANCE.scheduleCyclic(EventNotifications::timeUpdate, 20);
+
+ SkyblockEvents.JOIN.register(EventNotifications::refreshEvents);
+ ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> dispatcher.register(
+ ClientCommandManager.literal("skyblocker").then(
+ ClientCommandManager.literal("debug").then(
+ ClientCommandManager.literal("toasts").then(
+ ClientCommandManager.argument("time", IntegerArgumentType.integer(0))
+ .then(ClientCommandManager.argument("jacob", BoolArgumentType.bool()).executes(context -> {
+ long time = System.currentTimeMillis() / 1000 + context.getArgument("time", int.class);
+ if (context.getArgument("jacob", Boolean.class)) {
+ MinecraftClient.getInstance().getToastManager().add(
+ new JacobEventToast(time, "Jacob's farming contest", new String[]{"Cactus", "Cocoa Beans", "Pumpkin"})
+ );
+ } else {
+ MinecraftClient.getInstance().getToastManager().add(
+ new EventToast(time, "Jacob's or something idk", new ItemStack(Items.PAPER))
+ );
+ }
+ return 0;
+ }
+ )
+ )
+ )
+ )
+ )
+ ));
+ }
+
+ private static final Map<String, LinkedList<SkyblockEvent>> events = new Object2ObjectOpenHashMap<>();
+
+ public static Map<String, LinkedList<SkyblockEvent>> getEvents() {
+ return events;
+ }
+
+ public static void refreshEvents() {
+ CompletableFuture.supplyAsync(() -> {
+ try {
+ JsonArray jsonElements = SkyblockerMod.GSON.fromJson(Http.sendGetRequest("https://hysky.de/api/calendar"), JsonArray.class);
+ return jsonElements.asList().stream().map(JsonElement::getAsJsonObject).toList();
+ } catch (Exception e) {
+ LOGGER.error("[Skyblocker] Failed to download events list", e);
+ }
+ return List.<JsonObject>of();
+ }).thenAccept(eventsList -> {
+ events.clear();
+ for (JsonObject object : eventsList) {
+ if (object.get("timestamp").getAsLong() + object.get("duration").getAsInt() < currentTime) continue;
+ SkyblockEvent skyblockEvent = SkyblockEvent.of(object);
+ events.computeIfAbsent(object.get("event").getAsString(), s -> new LinkedList<>()).add(skyblockEvent);
+ }
+
+ for (Map.Entry<String, LinkedList<SkyblockEvent>> entry : events.entrySet()) {
+ entry.getValue().sort(Comparator.comparingLong(SkyblockEvent::start)); // Sort just in case it's not in order for some reason in API
+ //LOGGER.info("Next {} is at {}", entry.getKey(), entry.getValue().peekFirst());
+ }
+
+ for (String s : events.keySet()) {
+ SkyblockerConfigManager.get().eventNotifications.eventsReminderTimes.computeIfAbsent(s, s1 -> DEFAULT_REMINDERS);
+ }
+ }).exceptionally(EventNotifications::itBorked);
+ }
+
+ private static Void itBorked(Throwable throwable) {
+ LOGGER.error("[Skyblocker] Event loading borked, sowwy :(", throwable);
+ return null;
+ }
+
+
+ private static void timeUpdate() {
+
+ long newTime = System.currentTimeMillis() / 1000;
+ for (Map.Entry<String, LinkedList<SkyblockEvent>> entry : events.entrySet()) {
+ LinkedList<SkyblockEvent> nextEvents = entry.getValue();
+ SkyblockEvent skyblockEvent = nextEvents.peekFirst();
+ if (skyblockEvent == null) continue;
+
+ // Remove finished event
+ if (newTime > skyblockEvent.start() + skyblockEvent.duration()) {
+ nextEvents.pollFirst();
+ skyblockEvent = nextEvents.peekFirst();
+ if (skyblockEvent == null) continue;
+ }
+ String eventName = entry.getKey();
+ List<Integer> reminderTimes = SkyblockerConfigManager.get().eventNotifications.eventsReminderTimes.getOrDefault(eventName, DEFAULT_REMINDERS);
+ if (reminderTimes.isEmpty()) continue;
+
+ for (Integer reminderTime : reminderTimes) {
+ if (currentTime + reminderTime < skyblockEvent.start() && newTime + reminderTime >= skyblockEvent.start()) {
+ MinecraftClient instance = MinecraftClient.getInstance();
+ if (eventName.equals(JACOBS)) {
+ instance.getToastManager().add(
+ new JacobEventToast(skyblockEvent.start(), eventName, skyblockEvent.extras())
+ );
+ } else {
+ instance.getToastManager().add(
+ new EventToast(skyblockEvent.start(), eventName, eventIcons.getOrDefault(eventName, new ItemStack(Items.PAPER)))
+ );
+ }
+ SoundEvent soundEvent = SkyblockerConfigManager.get().eventNotifications.reminderSound.getSoundEvent();
+ if (soundEvent != null)
+ instance.getSoundManager().play(PositionedSoundInstance.master(soundEvent, 1f, 1f));
+ break;
+ }
+ }
+ }
+ currentTime = newTime;
+ }
+
+ public record SkyblockEvent(long start, int duration, String[] extras, @Nullable String warpCommand) {
+ public static SkyblockEvent of(JsonObject jsonObject) {
+ String location = jsonObject.get("location").getAsString();
+ location = location.isBlank() ? null : location;
+ return new SkyblockEvent(jsonObject.get("timestamp").getAsLong(),
+ jsonObject.get("duration").getAsInt(),
+ jsonObject.get("extras").getAsJsonArray().asList().stream().map(JsonElement::getAsString).toArray(String[]::new),
+ location);
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/events/EventToast.java b/src/main/java/de/hysky/skyblocker/skyblock/events/EventToast.java
new file mode 100644
index 00000000..567c800a
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/events/EventToast.java
@@ -0,0 +1,93 @@
+package de.hysky.skyblocker.skyblock.events;
+
+import de.hysky.skyblocker.SkyblockerMod;
+import de.hysky.skyblocker.utils.Utils;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.font.TextRenderer;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.toast.Toast;
+import net.minecraft.client.toast.ToastManager;
+import net.minecraft.item.ItemStack;
+import net.minecraft.text.MutableText;
+import net.minecraft.text.OrderedText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Colors;
+import net.minecraft.util.Formatting;
+import net.minecraft.util.Identifier;
+
+import java.util.List;
+
+public class EventToast implements Toast {
+ protected static final Identifier TEXTURE = new Identifier(SkyblockerMod.NAMESPACE, "notification");
+
+ private final long eventStartTime;
+
+ protected final List<OrderedText> message;
+ protected final List<OrderedText> messageNow;
+ protected final int messageWidth;
+ protected final int messageNowWidth;
+ protected final ItemStack icon;
+
+ protected boolean started;
+
+ public EventToast(long eventStartTime, String name, ItemStack icon) {
+ this.eventStartTime = eventStartTime;
+ MutableText formatted = Text.translatable("skyblocker.events.startsSoon", Text.literal(name).formatted(Formatting.YELLOW)).formatted(Formatting.WHITE);
+ TextRenderer renderer = MinecraftClient.getInstance().textRenderer;
+ message = renderer.wrapLines(formatted, 150);
+ messageWidth = message.stream().mapToInt(renderer::getWidth).max().orElse(150);
+
+ MutableText formattedNow = Text.translatable("skyblocker.events.startsNow", Text.literal(name).formatted(Formatting.YELLOW)).formatted(Formatting.WHITE);
+ messageNow = renderer.wrapLines(formattedNow, 150);
+ messageNowWidth = messageNow.stream().mapToInt(renderer::getWidth).max().orElse(150);
+ this.icon = icon;
+ this.started = eventStartTime - System.currentTimeMillis() / 1000 < 0;
+
+ }
+ @Override
+ public Visibility draw(DrawContext context, ToastManager manager, long startTime) {
+ context.drawGuiTexture(TEXTURE, 0, 0, getWidth(), getHeight());
+
+ int y = (getHeight() - getInnerContentsHeight())/2;
+ y = 2 + drawMessage(context, 30, y, Colors.WHITE);
+ drawTimer(context, 30, y);
+
+ context.drawItemWithoutEntity(icon, 8, getHeight()/2 - 8);
+ return startTime > 5_000 ? Visibility.HIDE: Visibility.SHOW;
+ }
+
+ protected int drawMessage(DrawContext context, int x, int y, int color) {
+ TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer;
+ for (OrderedText orderedText : started ? messageNow: message) {
+ context.drawText(textRenderer, orderedText, x, y, color, false);
+ y += textRenderer.fontHeight;
+ }
+ return y;
+ }
+
+ protected void drawTimer(DrawContext context, int x, int y) {
+ long currentTime = System.currentTimeMillis() / 1000;
+ int timeTillEvent = (int) (eventStartTime - currentTime);
+ started = timeTillEvent < 0;
+ if (started) return;
+
+ Text time = Utils.getDurationText(timeTillEvent);
+
+ TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer;
+ context.drawText(textRenderer, time, x, y, Colors.LIGHT_YELLOW, false);
+ }
+
+ @Override
+ public int getWidth() {
+ return (started ? messageNowWidth: messageWidth) + 30 + 6;
+ }
+
+ protected int getInnerContentsHeight() {
+ return message.size() * 9 + (started ? 0 : 9);
+ }
+
+ @Override
+ public int getHeight() {
+ return Math.max(getInnerContentsHeight() + 12 + 2, 32);
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/events/JacobEventToast.java b/src/main/java/de/hysky/skyblocker/skyblock/events/JacobEventToast.java
new file mode 100644
index 00000000..43ed7d12
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/events/JacobEventToast.java
@@ -0,0 +1,60 @@
+package de.hysky.skyblocker.skyblock.events;
+
+import de.hysky.skyblocker.skyblock.tabhud.widget.JacobsContestWidget;
+import de.hysky.skyblocker.utils.render.RenderHelper;
+import net.minecraft.client.font.TextRenderer;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.toast.ToastManager;
+import net.minecraft.client.util.math.MatrixStack;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.Items;
+import net.minecraft.util.Colors;
+import net.minecraft.util.math.MathHelper;
+
+public class JacobEventToast extends EventToast {
+
+ private final String[] crops;
+
+ private static final ItemStack DEFAULT_ITEM = new ItemStack(Items.IRON_HOE);
+
+ public JacobEventToast(long eventStartTime, String name, String[] crops) {
+ super(eventStartTime, name, new ItemStack(Items.IRON_HOE));
+ this.crops = crops;
+ }
+
+ @Override
+ public Visibility draw(DrawContext context, ToastManager manager, long startTime) {
+ context.drawGuiTexture(TEXTURE, 0, 0, getWidth(), getHeight());
+
+ int y = (getHeight() - getInnerContentsHeight()) / 2;
+ TextRenderer textRenderer = manager.getClient().textRenderer;
+ MatrixStack matrices = context.getMatrices();
+ if (startTime < 3_000) {
+ int k = MathHelper.floor(Math.clamp((3_000 - startTime) / 200.0f, 0.0f, 1.0f) * 255.0f) << 24 | 0x4000000;
+ y = 2 + drawMessage(context, 30, y, 0xFFFFFF | k);
+ } else {
+ int k = (~MathHelper.floor(Math.clamp((startTime - 3_000) / 200.0f, 0.0f, 1.0f) * 255.0f)) << 24 | 0x4000000;
+
+
+ String s = "Crops:";
+ int x = 30 + textRenderer.getWidth(s) + 4;
+ context.drawText(textRenderer, s, 30, 7 + (16 - textRenderer.fontHeight) / 2, Colors.WHITE, false);
+ for (int i = 0; i < crops.length; i++) {
+ context.drawItem(JacobsContestWidget.FARM_DATA.getOrDefault(crops[i], DEFAULT_ITEM), x + i * (16 + 8), 7);
+ }
+ // IDK how to make the items transparent, so I just redraw the texture on top
+ matrices.push();
+ matrices.translate(0, 0, 400f);
+ RenderHelper.renderNineSliceColored(context, TEXTURE, 0, 0, getWidth(), getHeight(), 1f, 1f, 1f, (k >> 24) / 255f);
+ matrices.pop();
+ y += textRenderer.fontHeight * message.size();
+ }
+ matrices.push();
+ matrices.translate(0, 0, 400f);
+ drawTimer(context, 30, y);
+
+ context.drawItemWithoutEntity(icon, 8, getHeight() / 2 - 8);
+ matrices.pop();
+ return startTime > 5_000 ? Visibility.HIDE : Visibility.SHOW;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/garden/VisitorHelper.java b/src/main/java/de/hysky/skyblocker/skyblock/garden/VisitorHelper.java
index 682933f4..d8f4dad7 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/garden/VisitorHelper.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/garden/VisitorHelper.java
@@ -2,6 +2,7 @@ package de.hysky.skyblocker.skyblock.garden;
import de.hysky.skyblocker.config.SkyblockerConfigManager;
import de.hysky.skyblocker.skyblock.itemlist.ItemRepository;
+import de.hysky.skyblocker.utils.Constants;
import de.hysky.skyblocker.utils.ItemUtils;
import de.hysky.skyblocker.utils.NEURepoManager;
import de.hysky.skyblocker.utils.Utils;
@@ -12,6 +13,7 @@ import it.unimi.dsi.fastutil.objects.Object2IntMap;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import it.unimi.dsi.fastutil.objects.ObjectObjectImmutablePair;
import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents;
+import net.minecraft.client.MinecraftClient;
import net.minecraft.client.font.TextRenderer;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.screen.ingame.HandledScreen;
@@ -40,7 +42,8 @@ public class VisitorHelper {
private static final Map<String, ItemStack> itemCache = new HashMap<>();
private static final int TEXT_START_X = 4;
private static final int TEXT_START_Y = 4;
- private static final int TEXT_INDENT = 8;
+ private static final int ENTRY_INDENT = 8;
+ private static final int ITEM_INDENT = 20;
private static final int LINE_SPACING = 3;
public static void init() {
@@ -59,22 +62,32 @@ public class VisitorHelper {
public static void onMouseClicked(double mouseX, double mouseY, int mouseButton, TextRenderer textRenderer) {
int yPosition = TEXT_START_Y;
-
for (Map.Entry<Pair<String, String>, Object2IntMap<String>> visitorEntry : itemMap.entrySet()) {
- int textWidth;
- int textHeight = textRenderer.fontHeight;
-
- yPosition += LINE_SPACING + textHeight;
+ yPosition += LINE_SPACING + textRenderer.fontHeight;
for (Object2IntMap.Entry<String> itemEntry : visitorEntry.getValue().object2IntEntrySet()) {
String itemText = itemEntry.getKey();
- textWidth = textRenderer.getWidth(itemText + " x" + itemEntry.getIntValue());
+ int textWidth = textRenderer.getWidth(itemText + " x" + itemEntry.getIntValue());
- if (isMouseOverText(mouseX, mouseY, TEXT_START_X + TEXT_INDENT, yPosition, textWidth, textHeight)) {
+ // Check if the mouse is over the item text
+ // The text starts at `TEXT_START_X + ENTRY_INDENT + ITEM_INDENT`
+ if (isMouseOverText(mouseX, mouseY, TEXT_START_X + ENTRY_INDENT + ITEM_INDENT, yPosition, textWidth, textRenderer.fontHeight)) {
+ // Send command to buy the item from the bazaar
MessageScheduler.INSTANCE.sendMessageAfterCooldown("/bz " + itemText);
return;
}
- yPosition += LINE_SPACING + textHeight;
+
+ // Check if the mouse is over the copy amount text
+ // The copy amount text starts at `TEXT_START_X + ENTRY_INDENT + ITEM_INDENT + textWidth`
+ MinecraftClient client = MinecraftClient.getInstance();
+ if (client.player != null && isMouseOverText(mouseX, mouseY, TEXT_START_X + ENTRY_INDENT + ITEM_INDENT + textWidth, yPosition, textRenderer.getWidth(" [Copy Amount]"), textRenderer.fontHeight)) {
+ // Copy the amount to the clipboard
+ client.keyboard.setClipboard(String.valueOf(itemEntry.getIntValue()));
+ client.player.sendMessage(Constants.PREFIX.get().append("Copied amount successfully"), false);
+ return;
+ }
+
+ yPosition += LINE_SPACING + textRenderer.fontHeight;
}
}
}
@@ -112,7 +125,7 @@ public class VisitorHelper {
}
}
- private static void updateItemMap(String visitorName, @Nullable String visitorTexture, Text lore) {
+ private static void updateItemMap(String visitorName, @Nullable String visitorTexture, Text lore) {
String[] splitItemText = lore.getString().split(" x");
String itemName = splitItemText[0].trim();
if (itemName.isEmpty()) return;
@@ -168,12 +181,23 @@ public class VisitorHelper {
return stack;
}
- private static void drawItemEntryWithHover(DrawContext context, TextRenderer textRenderer, @Nullable ItemStack stack, String itemName, int amount, int index, int mouseX, int mousseY) {
- Text text = stack != null ? stack.getName().copy().append(" x" + amount) : Text.literal(itemName + " x" + amount);
- drawTextWithOptionalUnderline(context, textRenderer, text, TEXT_START_X + TEXT_INDENT, TEXT_START_Y + (index * (LINE_SPACING + textRenderer.fontHeight)), mouseX, mousseY);
+ /**
+ * Draws the item entry, amount, and copy amount text with optional underline and the item icon
+ */
+ private static void drawItemEntryWithHover(DrawContext context, TextRenderer textRenderer, @Nullable ItemStack stack, String itemName, int amount, int index, int mouseX, int mouseY) {
+ Text text = stack != null ? stack.getName().copy().append(" x" + amount) : Text.literal(itemName + " x" + amount);
+ Text copyAmount = Text.literal(" [Copy Amount]");
+
+ // Calculate the y position of the text with index as the line number
+ int y = TEXT_START_Y + index * (LINE_SPACING + textRenderer.fontHeight);
+ // Draw the item and amount text
+ drawTextWithOptionalUnderline(context, textRenderer, text, TEXT_START_X + ENTRY_INDENT + ITEM_INDENT, y, mouseX, mouseY);
+ // Draw the copy amount text separately after the item and amount text
+ drawTextWithOptionalUnderline(context, textRenderer, copyAmount, TEXT_START_X + ENTRY_INDENT + ITEM_INDENT + textRenderer.getWidth(text), y, mouseX, mouseY);
+
// drawItem adds 150 to the z, which puts our z at 350, above the item in the slot (250) and their text (300) and below the cursor stack (382) and their text (432)
if (stack != null) {
- context.drawItem(stack, TEXT_START_X + TEXT_INDENT + 2 + textRenderer.getWidth(text), TEXT_START_Y + (index * (LINE_SPACING + textRenderer.fontHeight)) - textRenderer.fontHeight + 5);
+ context.drawItem(stack, TEXT_START_X + ENTRY_INDENT, y - textRenderer.fontHeight + 5);
}
}
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..031817ac 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;
@@ -129,17 +128,18 @@ public class ItemTooltip {
}
}
- final Map<Integer, String> itemTierFloors = Map.of(
- 1, "F1",
- 2, "F2",
- 3, "F3",
- 4, "F4/M1",
- 5, "F5/M2",
- 6, "F6/M3",
- 7, "F7/M4",
- 8, "M5",
- 9, "M6",
- 10, "M7"
+ final Map<Integer, String> itemTierFloors = Map.ofEntries(
+ Map.entry(0, "E"),
+ Map.entry(1, "F1"),
+ Map.entry(2, "F2"),
+ Map.entry(3, "F3"),
+ Map.entry(4, "F4/M1"),
+ Map.entry(5, "F5/M2"),
+ Map.entry(6, "F6/M3"),
+ Map.entry(7, "F7/M4"),
+ Map.entry(8, "M5"),
+ Map.entry(9, "M6"),
+ Map.entry(10, "M7")
);
if (SkyblockerConfigManager.get().general.itemTooltip.dungeonQuality) {
@@ -394,15 +394,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;
@@ -448,4 +456,4 @@ public class ItemTooltip {
});
}, 1200, true);
}
-} \ No newline at end of file
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemListTab.java b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemListTab.java
new file mode 100644
index 00000000..4109246d
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemListTab.java
@@ -0,0 +1,89 @@
+package de.hysky.skyblocker.skyblock.itemlist;
+
+import com.mojang.blaze3d.systems.RenderSystem;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.Element;
+import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder;
+import net.minecraft.client.gui.widget.TextFieldWidget;
+import net.minecraft.client.util.math.MatrixStack;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+import java.util.List;
+
+public class ItemListTab extends ItemListWidget.TabContainerWidget {
+
+ private SearchResultsWidget results;
+ private final MinecraftClient client;
+ private TextFieldWidget searchField;
+
+ public ItemListTab(int x, int y, MinecraftClient client, TextFieldWidget searchField) {
+ super(x, y, Text.literal("Item List Tab"));
+ this.client = client;
+ this.searchField = searchField;
+ if (ItemRepository.filesImported()) {
+ this.results = new SearchResultsWidget(this.client, x - 9, y - 9 );
+ this.results.updateSearchResult(searchField == null ? "": this.searchField.getText());
+ }
+ }
+
+ @Override
+ public List<? extends Element> children() {
+ return List.of(results, searchField);
+ }
+
+ @Override
+ protected void renderWidget(DrawContext context, int mouseX, int mouseY, float delta) {
+ MatrixStack matrices = context.getMatrices();
+ matrices.push();
+ matrices.translate(0.0D, 0.0D, 100.0D);
+ RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F);
+ int x = getX();
+ int y = getY();
+
+ // all coordinates offseted -9
+ if (!ItemRepository.filesImported() && !this.searchField.isFocused() && this.searchField.getText().isEmpty()) {
+ Text hintText = (Text.literal("Loading...")).formatted(Formatting.ITALIC).formatted(Formatting.GRAY);
+ context.drawTextWithShadow(this.client.textRenderer, hintText, x + 16, y + 7, -1);
+ } else if (!this.searchField.isFocused() && this.searchField.getText().isEmpty()) {
+ Text hintText = (Text.translatable("gui.recipebook.search_hint")).formatted(Formatting.ITALIC).formatted(Formatting.GRAY);
+ context.drawTextWithShadow(this.client.textRenderer, hintText, x + 16, y + 7, -1);
+ } else {
+ this.searchField.render(context, mouseX, mouseY, delta);
+ }
+ if (ItemRepository.filesImported()) {
+ if (results == null) {
+ this.results = new SearchResultsWidget(this.client, x - 9, y - 9);
+ }
+ this.results.updateSearchResult(this.searchField.getText());
+ this.results.render(context, mouseX, mouseY, delta);
+ }
+ matrices.pop();
+ }
+
+ @Override
+ protected void appendClickableNarrations(NarrationMessageBuilder builder) {}
+
+ public void setSearchField(TextFieldWidget searchField) {
+ this.searchField = searchField;
+ }
+
+ @Override
+ public boolean mouseClicked(double mouseX, double mouseY, int button) {
+ if (!visible) return false;
+ if (this.searchField.mouseClicked(mouseX, mouseY, button)) {
+ this.results.closeRecipeView();
+ this.searchField.setFocused(true);
+ return true;
+ } else {
+ this.searchField.setFocused(false);
+ return this.results.mouseClicked(mouseX, mouseY, button);
+ }
+ }
+
+ @Override
+ public void drawTooltip(DrawContext context, int mouseX, int mouseY) {
+ if (this.results != null) this.results.drawTooltip(context, mouseX, mouseY);
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemListWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemListWidget.java
index 6120528c..a618f4df 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemListWidget.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemListWidget.java
@@ -1,103 +1,135 @@
package de.hysky.skyblocker.skyblock.itemlist;
-import com.mojang.blaze3d.systems.RenderSystem;
-
import de.hysky.skyblocker.mixins.accessors.RecipeBookWidgetAccessor;
+import de.hysky.skyblocker.utils.render.gui.SideTabButtonWidget;
+import it.unimi.dsi.fastutil.Pair;
+import it.unimi.dsi.fastutil.objects.ObjectObjectImmutablePair;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.screen.recipebook.RecipeBookWidget;
+import net.minecraft.client.gui.tooltip.Tooltip;
+import net.minecraft.client.gui.widget.ContainerWidget;
import net.minecraft.client.gui.widget.TextFieldWidget;
-import net.minecraft.client.util.math.MatrixStack;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.Items;
import net.minecraft.screen.AbstractRecipeScreenHandler;
import net.minecraft.text.Text;
-import net.minecraft.util.Formatting;
+
+import java.util.ArrayList;
+import java.util.List;
@Environment(value = EnvType.CLIENT)
public class ItemListWidget extends RecipeBookWidget {
private int parentWidth;
private int parentHeight;
private int leftOffset;
- private TextFieldWidget searchField;
- private SearchResultsWidget results;
+
+ private TabContainerWidget currentTabContent;
+ private final List<Pair<SideTabButtonWidget, TabContainerWidget>> tabs = new ArrayList<>(2);
+ private ItemListTab itemListTab;
+
+ private static int currentTab = 0;
public ItemListWidget() {
super();
}
- public void updateSearchResult() {
- this.results.updateSearchResult(((RecipeBookWidgetAccessor) this).getSearchText());
- }
-
@Override
public void initialize(int parentWidth, int parentHeight, MinecraftClient client, boolean narrow, AbstractRecipeScreenHandler<?> craftingScreenHandler) {
super.initialize(parentWidth, parentHeight, client, narrow, craftingScreenHandler);
this.parentWidth = parentWidth;
this.parentHeight = parentHeight;
this.leftOffset = narrow ? 0 : 86;
- this.searchField = ((RecipeBookWidgetAccessor) this).getSearchField();
- int x = (this.parentWidth - 147) / 2 - this.leftOffset;
- int y = (this.parentHeight - 166) / 2;
- if (ItemRepository.filesImported()) {
- this.results = new SearchResultsWidget(this.client, x, y);
- this.updateSearchResult();
- }
+ TextFieldWidget searchField = ((RecipeBookWidgetAccessor) this).getSearchField();
+ int x = (parentWidth - 147) / 2 - leftOffset;
+ int y = (parentHeight - 166) / 2;
+
+ // Init all the tabs, content and the tab button on the left
+ tabs.clear();
+
+ // Item List
+ itemListTab = new ItemListTab(x + 9, y + 9, this.client, searchField);
+ SideTabButtonWidget itemListTabButton = new SideTabButtonWidget(x - 30, y + 3, currentTab == 0, new ItemStack(Items.CRAFTING_TABLE));
+ itemListTabButton.setTooltip(Tooltip.of(Text.literal("Item List")));
+ if (currentTab == 0) currentTabContent = itemListTab;
+ tabs.add(new ObjectObjectImmutablePair<>(
+ itemListTabButton,
+ this.itemListTab));
+
+ // Upcoming Events
+ UpcomingEventsTab upcomingEventsTab = new UpcomingEventsTab(x + 9, y + 9, this.client);
+ SideTabButtonWidget eventsTabButtonWidget = new SideTabButtonWidget(x - 30, y + 3 + 27, currentTab == 1, new ItemStack(Items.CLOCK));
+ eventsTabButtonWidget.setTooltip(Tooltip.of(Text.literal("Upcoming Events")));
+ if (currentTab == 1) currentTabContent = upcomingEventsTab;
+ tabs.add(new ObjectObjectImmutablePair<>(
+ eventsTabButtonWidget,
+ upcomingEventsTab
+ ));
+
+ }
+
+ @Override
+ public void reset() {
+ super.reset();
+ if (itemListTab != null) itemListTab.setSearchField(((RecipeBookWidgetAccessor) this).getSearchField());
}
@Override
public void render(DrawContext context, int mouseX, int mouseY, float delta) {
if (this.isOpen()) {
- MatrixStack matrices = context.getMatrices();
- matrices.push();
- matrices.translate(0.0D, 0.0D, 100.0D);
- RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F);
- this.searchField = ((RecipeBookWidgetAccessor) this).getSearchField();
int i = (this.parentWidth - 147) / 2 - this.leftOffset;
int j = (this.parentHeight - 166) / 2;
+ // Draw the texture
context.drawTexture(TEXTURE, i, j, 1, 1, 147, 166);
- this.searchField = ((RecipeBookWidgetAccessor) this).getSearchField();
-
- if (!ItemRepository.filesImported() && !this.searchField.isFocused() && this.searchField.getText().isEmpty()) {
- Text hintText = (Text.literal("Loading...")).formatted(Formatting.ITALIC).formatted(Formatting.GRAY);
- context.drawTextWithShadow(this.client.textRenderer, hintText, i + 25, j + 14, -1);
- } else if (!this.searchField.isFocused() && this.searchField.getText().isEmpty()) {
- Text hintText = (Text.translatable("gui.recipebook.search_hint")).formatted(Formatting.ITALIC).formatted(Formatting.GRAY);
- context.drawTextWithShadow(this.client.textRenderer, hintText, i + 25, j + 14, -1);
- } else {
- this.searchField.render(context, mouseX, mouseY, delta);
- }
- if (ItemRepository.filesImported()) {
- if (results == null) {
- int x = (this.parentWidth - 147) / 2 - this.leftOffset;
- int y = (this.parentHeight - 166) / 2;
- this.results = new SearchResultsWidget(this.client, x, y);
- }
- this.updateSearchResult();
- this.results.render(context, mouseX, mouseY, delta);
+ // Draw the tab's content
+ if (currentTabContent != null) currentTabContent.render(context, mouseX, mouseY, delta);
+ // Draw the tab buttons
+ for (Pair<SideTabButtonWidget, TabContainerWidget> tab : tabs) {
+ tab.left().render(context, mouseX, mouseY, delta);
}
- matrices.pop();
+
}
}
@Override
public void drawTooltip(DrawContext context, int x, int y, int mouseX, int mouseY) {
- if (this.isOpen() && ItemRepository.filesImported() && results != null) {
- this.results.drawTooltip(context, mouseX, mouseY);
+ if (this.isOpen() && currentTabContent != null) {
+ this.currentTabContent.drawTooltip(context, mouseX, mouseY);
}
}
@Override
public boolean mouseClicked(double mouseX, double mouseY, int button) {
- if (this.isOpen() && this.client.player != null && !this.client.player.isSpectator() && ItemRepository.filesImported() && this.searchField != null && results != null) {
- if (this.searchField.mouseClicked(mouseX, mouseY, button)) {
- this.results.closeRecipeView();
- this.searchField.setFocused(true);
- return true;
- } else {
- this.searchField.setFocused(false);
- return this.results.mouseClicked(mouseX, mouseY, button);
+ if (this.isOpen() && this.client.player != null && !this.client.player.isSpectator()) {
+ // check if a tab is clicked
+ for (Pair<SideTabButtonWidget, TabContainerWidget> tab : tabs) {
+ if (tab.first().mouseClicked(mouseX, mouseY, button) && currentTabContent != tab.right()) {
+ for (Pair<SideTabButtonWidget, TabContainerWidget> tab2 : tabs) {
+ tab2.first().setToggled(false);
+ }
+ tab.first().setToggled(true);
+ currentTabContent = tab.right();
+ currentTab = tabs.indexOf(tab);
+ return true;
+ }
}
+ // click the tab content
+ if (currentTabContent != null) return currentTabContent.mouseClicked(mouseX, mouseY, button);
+ else return false;
} else return false;
}
+
+ /**
+ * A container widget but with a fixed width and height and a drawTooltip method to implement
+ */
+ public abstract static class TabContainerWidget extends ContainerWidget {
+
+ public TabContainerWidget(int x, int y, Text text) {
+ super(x, y, 131, 150, text);
+ }
+
+ public abstract void drawTooltip(DrawContext context, int mouseX, int mouseY);
+ }
} \ No newline at end of file
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/itemlist/SearchResultsWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/SearchResultsWidget.java
index 1ef352e3..48d3a8f6 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/itemlist/SearchResultsWidget.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/SearchResultsWidget.java
@@ -6,6 +6,7 @@ import net.minecraft.client.MinecraftClient;
import net.minecraft.client.font.TextRenderer;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.Drawable;
+import net.minecraft.client.gui.Element;
import net.minecraft.client.gui.screen.ButtonTextures;
import net.minecraft.client.gui.widget.ToggleButtonWidget;
import net.minecraft.component.DataComponentTypes;
@@ -23,7 +24,7 @@ import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
-public class SearchResultsWidget implements Drawable {
+public class SearchResultsWidget implements Drawable, Element {
private static final ButtonTextures PAGE_FORWARD_TEXTURES = new ButtonTextures(new Identifier("recipe_book/page_forward"), new Identifier("recipe_book/page_forward_highlighted"));
private static final ButtonTextures PAGE_BACKWARD_TEXTURES = new ButtonTextures(new Identifier("recipe_book/page_backward"), new Identifier("recipe_book/page_backward_highlighted"));
private static final int COLS = 5;
@@ -225,4 +226,16 @@ public class SearchResultsWidget implements Drawable {
return false;
}
+ private boolean focused = false;
+
+ @Override
+ public void setFocused(boolean focused) {
+ this.focused = focused;
+ }
+
+ @Override
+ public boolean isFocused() {
+ return focused;
+ }
+
}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/itemlist/UpcomingEventsTab.java b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/UpcomingEventsTab.java
new file mode 100644
index 00000000..9552ae87
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/UpcomingEventsTab.java
@@ -0,0 +1,168 @@
+package de.hysky.skyblocker.skyblock.itemlist;
+
+import de.hysky.skyblocker.mixins.accessors.DrawContextInvoker;
+import de.hysky.skyblocker.skyblock.events.EventNotifications;
+import de.hysky.skyblocker.skyblock.tabhud.widget.JacobsContestWidget;
+import de.hysky.skyblocker.utils.Utils;
+import de.hysky.skyblocker.utils.scheduler.MessageScheduler;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.font.TextRenderer;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.Element;
+import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder;
+import net.minecraft.client.gui.tooltip.HoveredTooltipPositioner;
+import net.minecraft.client.gui.tooltip.TooltipComponent;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.Items;
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Style;
+import net.minecraft.text.Text;
+import net.minecraft.util.Colors;
+import net.minecraft.util.Formatting;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.LinkedList;
+import java.util.List;
+
+public class UpcomingEventsTab extends ItemListWidget.TabContainerWidget {
+ private static final ItemStack CLOCK = new ItemStack(Items.CLOCK);
+ private final MinecraftClient client;
+ private final List<EventRenderer> events;
+
+ public UpcomingEventsTab(int x, int y, MinecraftClient client) {
+ super(x, y, Text.literal("Upcoming Events Tab"));
+ this.client = client;
+ events = EventNotifications.getEvents().entrySet()
+ .stream()
+ .sorted(Comparator.comparingLong(a -> a.getValue().isEmpty() ? Long.MAX_VALUE : a.getValue().peekFirst().start()))
+ .map(stringLinkedListEntry -> new EventRenderer(stringLinkedListEntry.getKey(), stringLinkedListEntry.getValue()))
+ .toList();
+ }
+
+ @Override
+ public void drawTooltip(DrawContext context, int mouseX, int mouseY) {
+ if (hovered != null) {
+ ((DrawContextInvoker) context).invokeDrawTooltip(this.client.textRenderer, hovered.getTooltip(), mouseX, mouseY, HoveredTooltipPositioner.INSTANCE);
+ }
+ }
+
+ @Override
+ public List<? extends Element> children() {
+ return List.of();
+ }
+
+ private EventRenderer hovered = null;
+
+ @Override
+ protected void renderWidget(DrawContext context, int mouseX, int mouseY, float delta) {
+ int x = getX();
+ int y = getY();
+ context.enableScissor(x, y, getRight(), getBottom());
+ context.drawItem(CLOCK, x, y + 4);
+ context.drawText(this.client.textRenderer, "Upcoming Events", x + 17, y + 7, -1, true);
+
+ int eventsY = y + 7 + 24;
+ hovered = null;
+ for (EventRenderer eventRenderer : events) {
+ eventRenderer.render(context, x + 1, eventsY, mouseX, mouseY);
+ if (isMouseOver(mouseX, mouseY) && eventRenderer.isMouseOver(mouseX, mouseY, x+1, eventsY)) hovered = eventRenderer;
+ eventsY += eventRenderer.getHeight();
+
+ }
+ context.disableScissor();
+ }
+
+ @Override
+ public boolean mouseClicked(double mouseX, double mouseY, int button) {
+ if (hovered != null && hovered.getWarpCommand() != null) {
+ MessageScheduler.INSTANCE.sendMessageAfterCooldown(hovered.getWarpCommand());
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ protected void appendClickableNarrations(NarrationMessageBuilder builder) {
+
+ }
+
+ public static class EventRenderer {
+
+ private final LinkedList<EventNotifications.SkyblockEvent> events;
+ private final String eventName;
+
+ public EventRenderer(String eventName, LinkedList<EventNotifications.SkyblockEvent> events) {
+ this.events = events;
+ this.eventName = eventName;
+ }
+
+ public void render(DrawContext context, int x, int y, int mouseX, int mouseY) {
+ long time = System.currentTimeMillis() / 1000;
+ TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer;
+ context.drawText(textRenderer, Text.literal(eventName).fillStyle(Style.EMPTY.withUnderline(isMouseOver(mouseX, mouseY, x, y))), x, y, -1, true);
+ if (events.isEmpty()) {
+ context.drawText(textRenderer, Text.literal(" ").append(Text.translatable("skyblocker.events.tab.noMore")), x, y + textRenderer.fontHeight, Colors.GRAY, false);
+ } else if (events.peekFirst().start() > time) {
+ MutableText formatted = Text.literal(" ").append(Text.translatable("skyblocker.events.tab.startsIn", Utils.getDurationText((int) (events.peekFirst().start() - time)))).formatted(Formatting.YELLOW);
+ context.drawText(textRenderer, formatted, x, y + textRenderer.fontHeight, -1, true);
+ } else {
+ MutableText formatted = Text.literal(" ").append(Text.translatable( "skyblocker.events.tab.endsIn", Utils.getDurationText((int) (events.peekFirst().start() + events.peekFirst().duration() - time)))).formatted(Formatting.GREEN);
+ context.drawText(textRenderer, formatted, x, y + textRenderer.fontHeight, -1, true);
+ }
+
+ }
+
+ public int getHeight() {
+ return 20;
+ }
+
+ public boolean isMouseOver(int mouseX, int mouseY, int x, int y) {
+ return mouseX >= x && mouseX <= x + 131 && mouseY >= y && mouseY <= y+getHeight();
+ }
+
+ public List<TooltipComponent> getTooltip() {
+ List<TooltipComponent> components = new ArrayList<>();
+ if (events.peekFirst() == null) return components;
+ if (eventName.equals(EventNotifications.JACOBS)) {
+ components.add(new JacobsTooltip(events.peekFirst().extras()));
+ }
+ //noinspection DataFlowIssue
+ if (events.peekFirst().warpCommand() != null) {
+ components.add(TooltipComponent.of(Text.translatable("skyblocker.events.tab.clickToWarp").formatted(Formatting.ITALIC).asOrderedText()));
+ }
+
+ return components;
+ }
+
+ public @Nullable String getWarpCommand() {
+ if (events.isEmpty()) return null;
+ return events.peek().warpCommand();
+ }
+ }
+
+ private record JacobsTooltip(String[] crops) implements TooltipComponent {
+
+ private static final ItemStack BARRIER = new ItemStack(Items.BARRIER);
+
+ @Override
+ public int getHeight() {
+ return 20;
+ }
+
+ @Override
+ public int getWidth(TextRenderer textRenderer) {
+ return 16 * 3 + 4;
+ }
+
+ @Override
+ public void drawItems(TextRenderer textRenderer, int x, int y, DrawContext context) {
+ for (int i = 0; i < crops.length; i++) {
+ String crop = crops[i];
+ context.drawItem(JacobsContestWidget.FARM_DATA.getOrDefault(crop, BARRIER), x + 18 * i, y + 2);
+ }
+ }
+
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/searchoverlay/OverlayScreen.java b/src/main/java/de/hysky/skyblocker/skyblock/searchoverlay/OverlayScreen.java
index a03f3549..b227ff01 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/searchoverlay/OverlayScreen.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/searchoverlay/OverlayScreen.java
@@ -3,9 +3,11 @@ package de.hysky.skyblocker.skyblock.searchoverlay;
import de.hysky.skyblocker.config.SkyblockerConfigManager;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.screen.Screen;
+import net.minecraft.client.gui.tooltip.Tooltip;
import net.minecraft.client.gui.widget.ButtonWidget;
import net.minecraft.client.gui.widget.TextFieldWidget;
import net.minecraft.item.ItemStack;
+import net.minecraft.text.MutableText;
import net.minecraft.text.Style;
import net.minecraft.text.Text;
import net.minecraft.util.Formatting;
@@ -17,9 +19,12 @@ import static de.hysky.skyblocker.skyblock.itemlist.ItemRepository.getItemStack;
public class OverlayScreen extends Screen {
protected static final Identifier SEARCH_ICON_TEXTURE = new Identifier("icon/search");
+ private static final Identifier BACKGROUND_TEXTURE = new Identifier("social_interactions/background");
private static final int rowHeight = 20;
private TextFieldWidget searchField;
private ButtonWidget finishedButton;
+ private ButtonWidget maxPetButton;
+ private ButtonWidget dungeonStarButton;
private ButtonWidget[] suggestionButtons;
private ButtonWidget[] historyButtons;
@@ -44,10 +49,11 @@ public class OverlayScreen extends Screen {
searchField.setMaxLength(30);
// finish buttons
- finishedButton = ButtonWidget.builder(Text.literal("").setStyle(Style.EMPTY.withColor(Formatting.GREEN)), a -> close())
+ finishedButton = ButtonWidget.builder(Text.literal(""), a -> close())
.position(startX + rowWidth - rowHeight, startY)
.size(rowHeight, rowHeight).build();
+
// suggested item buttons
int rowOffset = rowHeight;
int totalSuggestions = SkyblockerConfigManager.get().uiAndVisuals.searchOverlay.maxSuggestions;
@@ -80,6 +86,26 @@ public class OverlayScreen extends Screen {
break;
}
}
+ //auction only elements
+ if (SearchOverManager.isAuction) {
+ //max pet level button
+ maxPetButton = ButtonWidget.builder(Text.literal(""), a -> {
+ SearchOverManager.maxPetLevel = !SearchOverManager.maxPetLevel;
+ updateMaxPetText();
+ })
+ .tooltip(Tooltip.of(Text.translatable("skyblocker.config.general.searchOverlay.maxPet.@Tooltip")))
+ .position(startX, startY - rowHeight - 8)
+ .size(rowWidth / 2, rowHeight).build();
+ updateMaxPetText();
+
+ //dungeon star input
+ dungeonStarButton = ButtonWidget.builder(Text.literal("✪"), a -> updateStars())
+ .tooltip(Tooltip.of(Text.translatable("skyblocker.config.general.searchOverlay.starsTooltip")))
+ .position(startX + (int) (rowWidth * 0.5), startY - rowHeight - 8)
+ .size(rowWidth / 2, rowHeight).build();
+
+ updateStars();
+ }
//add drawables in order to make tab navigation sensible
addDrawableChild(searchField);
@@ -93,11 +119,86 @@ public class OverlayScreen extends Screen {
}
addDrawableChild(finishedButton);
+ if (SearchOverManager.isAuction) {
+ addDrawableChild(maxPetButton);
+ addDrawableChild(dungeonStarButton);
+ }
+
//focus the search box
this.setInitialFocus(searchField);
}
/**
+ * Finds if the mouse is clicked on the dungeon star button and if so works out what stars the user clicked on
+ *
+ * @param mouseX the X coordinate of the mouse
+ * @param mouseY the Y coordinate of the mouse
+ * @param button the mouse button number
+ * @return super
+ */
+ @Override
+ public boolean mouseClicked(double mouseX, double mouseY, int button) {
+ if (SearchOverManager.isAuction && dungeonStarButton.isHovered() && client != null) {
+ double actualTextWidth = client.textRenderer.getWidth(dungeonStarButton.getMessage());
+ double textOffset = (dungeonStarButton.getWidth() - actualTextWidth) / 2;
+ double offset = mouseX - (dungeonStarButton.getX() + textOffset);
+ int starCount = (int) ((offset / actualTextWidth) * 10);
+ starCount = Math.clamp(starCount + 1, 0, 10);
+ //if same as old value set stars to 0 else set to selected amount
+ if (starCount == SearchOverManager.dungeonStars) {
+ SearchOverManager.dungeonStars = 0;
+ } else {
+ SearchOverManager.dungeonStars = starCount;
+ }
+ }
+
+ return super.mouseClicked(mouseX, mouseY, button);
+ }
+
+ /**
+ * Updates the text displayed on the max pet level button to represent the settings current state
+ */
+ private void updateMaxPetText() {
+ if (SearchOverManager.maxPetLevel) {
+ maxPetButton.setMessage(Text.translatable("skyblocker.config.general.searchOverlay.maxPet").append(Text.literal(" ✔")).formatted(Formatting.GREEN));
+ } else {
+ maxPetButton.setMessage(Text.translatable("skyblocker.config.general.searchOverlay.maxPet").append(Text.literal(" ❌")).formatted(Formatting.RED));
+ }
+ }
+
+ /**
+ * Updates stars in dungeon star input to represent the current star value
+ */
+ private void updateStars() {
+ MutableText stars = Text.empty();
+ for (int i = 0; i < SearchOverManager.dungeonStars; i++) {
+ stars.append(Text.literal("✪").formatted(i < 5 ? Formatting.YELLOW : Formatting.RED));
+ }
+ for (int i = SearchOverManager.dungeonStars; i < 10; i++) {
+ stars.append(Text.literal("✪"));
+ }
+ dungeonStarButton.setMessage(stars);
+ }
+
+ /**
+ * Renders the background for the search using the social interactions background
+ * @param context context
+ * @param mouseX mouseX
+ * @param mouseY mouseY
+ * @param delta delta
+ */
+ @Override
+ public void renderBackground(DrawContext context, int mouseX, int mouseY, float delta) {
+ super.renderBackground(context, mouseX, mouseY, delta);
+ //find max height
+ int maxHeight = rowHeight * (1 + suggestionButtons.length + historyButtons.length);
+ if (historyButtons.length > 0) { //add space for history label if it could exist
+ maxHeight += (int) (rowHeight * 0.75);
+ }
+ context.drawGuiTexture(BACKGROUND_TEXTURE, searchField.getX() - 8, searchField.getY() - 8, (int) (this.width * 0.4) + 16, maxHeight + 16);
+ }
+
+ /**
* Renders the search icon, label for the history and item Stacks for item names
*/
@Override
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/searchoverlay/SearchOverManager.java b/src/main/java/de/hysky/skyblocker/skyblock/searchoverlay/SearchOverManager.java
index 917a6aa0..bb1875ba 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/searchoverlay/SearchOverManager.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/searchoverlay/SearchOverManager.java
@@ -10,6 +10,7 @@ import de.hysky.skyblocker.skyblock.item.tooltip.TooltipInfoType;
import de.hysky.skyblocker.utils.NEURepoManager;
import de.hysky.skyblocker.utils.scheduler.MessageScheduler;
import io.github.moulberry.repo.data.NEUItem;
+import io.github.moulberry.repo.util.NEUId;
import it.unimi.dsi.fastutil.Pair;
import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback;
import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource;
@@ -24,10 +25,7 @@ import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
+import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@@ -40,8 +38,7 @@ public class SearchOverManager {
private static final Logger LOGGER = LoggerFactory.getLogger("Skyblocker Search Overlay");
private static final Pattern BAZAAR_ENCHANTMENT_PATTERN = Pattern.compile("ENCHANTMENT_(\\D*)_(\\d+)");
- private static final Pattern AUCTION_PET_AND_RUNE_PATTERN = Pattern.compile("([A-Z0-9_]+);(\\d+)");
-
+ private static final String PET_NAME_START = "[Lvl {LVL}] ";
/**
* converts index (in array) +1 to a roman numeral
*/
@@ -52,14 +49,18 @@ public class SearchOverManager {
private static @Nullable SignBlockEntity sign = null;
private static boolean signFront = true;
- private static boolean isAuction;
+ protected static boolean isAuction;
private static boolean isCommand;
protected static String search = "";
+ protected static Boolean maxPetLevel = false;
+ protected static int dungeonStars = 0;
// Use non-final variables and swap them to prevent concurrent modification
private static HashSet<String> bazaarItems;
private static HashSet<String> auctionItems;
+ private static HashSet<String> auctionPets;
+ private static HashSet<String> starableItems;
private static HashMap<String, String> namesToId;
public static String[] suggestionsArray = {};
@@ -91,6 +92,8 @@ public class SearchOverManager {
private static void loadItems() {
HashSet<String> bazaarItems = new HashSet<>();
HashSet<String> auctionItems = new HashSet<>();
+ HashSet<String> auctionPets = new HashSet<>();
+ HashSet<String> starableItems = new HashSet<>();
HashMap<String, String> namesToId = new HashMap<>();
//get bazaar items
@@ -139,28 +142,28 @@ public class SearchOverManager {
//get auction items
try {
+ Set<@NEUId String> essenceCosts = NEURepoManager.NEU_REPO.getConstants().getEssenceCost().getCosts().keySet();
if (TooltipInfoType.THREE_DAY_AVERAGE.getData() == null) {
TooltipInfoType.THREE_DAY_AVERAGE.run();
}
for (Map.Entry<String, JsonElement> entry : TooltipInfoType.THREE_DAY_AVERAGE.getData().entrySet()) {
String id = entry.getKey();
-
- Matcher matcher = AUCTION_PET_AND_RUNE_PATTERN.matcher(id);
- if (matcher.find()) {//is a pet or rune convert id to name
- String name = matcher.group(1).replace("_", " ");
- name = capitalizeFully(name);
- auctionItems.add(name);
- namesToId.put(name, id);
- continue;
- }
- //something else look up in NEU repo.
+ //look up in NEU repo.
id = id.split("[+-]")[0];
NEUItem neuItem = NEURepoManager.NEU_REPO.getItems().getItemBySkyblockId(id);
if (neuItem != null) {
String name = Formatting.strip(neuItem.getDisplayName());
+ //add names that are pets to the list of pets to work with the lvl 100 button
+ if (name != null && name.startsWith(PET_NAME_START)) {
+ name = name.replace(PET_NAME_START, "");
+ auctionPets.add(name.toLowerCase());
+ }
+ //if it has essence cost add to starable items
+ if (name != null && essenceCosts.contains(neuItem.getSkyblockItemId())) {
+ starableItems.add(name.toLowerCase());
+ }
auctionItems.add(name);
namesToId.put(name, id);
- continue;
}
}
} catch (Exception e) {
@@ -169,11 +172,14 @@ public class SearchOverManager {
SearchOverManager.bazaarItems = bazaarItems;
SearchOverManager.auctionItems = auctionItems;
+ SearchOverManager.auctionPets = auctionPets;
+ SearchOverManager.starableItems = starableItems;
SearchOverManager.namesToId = namesToId;
}
/**
* Capitalizes the first letter off every word in a string
+ *
* @param str string to capitalize
*/
private static String capitalizeFully(String str) {
@@ -188,8 +194,9 @@ public class SearchOverManager {
/**
* Receives data when a search is started and resets values
- * @param sign the sign that is being edited
- * @param front if it's the front of the sign
+ *
+ * @param sign the sign that is being edited
+ * @param front if it's the front of the sign
* @param isAuction if the sign is loaded from the auction house menu or bazaar
*/
public static void updateSign(@NotNull SignBlockEntity sign, boolean front, boolean isAuction) {
@@ -214,6 +221,7 @@ public class SearchOverManager {
/**
* Updates the search value and the suggestions based on that value.
+ *
* @param newValue new search value
*/
protected static void updateSearch(String newValue) {
@@ -221,11 +229,30 @@ public class SearchOverManager {
//update the suggestion values
int totalSuggestions = SkyblockerConfigManager.get().uiAndVisuals.searchOverlay.maxSuggestions;
if (newValue.isBlank() || totalSuggestions == 0) return; //do not search for empty value
- suggestionsArray = (isAuction ? auctionItems : bazaarItems).stream().filter(item -> item.toLowerCase().contains(search.toLowerCase())).limit(totalSuggestions).toArray(String[]::new);
+ suggestionsArray = (isAuction ? auctionItems : bazaarItems).stream().sorted(Comparator.comparing(SearchOverManager::shouldFrontLoad, Comparator.reverseOrder())).filter(item -> item.toLowerCase().contains(search.toLowerCase())).limit(totalSuggestions).toArray(String[]::new);
+ }
+
+ /**
+ * determines if a value should be moved to the front of the search
+ *
+ * @param name name of the suggested item
+ * @return if the value should be at the front of the search queue
+ */
+ private static boolean shouldFrontLoad(String name) {
+ if (!isAuction) {
+ return false;
+ }
+ //do nothing to non pets
+ if (!auctionPets.contains(name.toLowerCase())) {
+ return false;
+ }
+ //only front load pets when there is enough of the pet typed, so it does not spoil searching for other items
+ return (double) search.length() / name.length() > 0.5;
}
/**
* Gets the suggestion in the suggestion array at the index
+ *
* @param index index of suggestion
*/
protected static String getSuggestion(int index) {
@@ -242,6 +269,7 @@ public class SearchOverManager {
/**
* Gets the item name in the history array at the index
+ *
* @param index index of suggestion
*/
protected static String getHistory(int index) {
@@ -286,13 +314,18 @@ public class SearchOverManager {
}
/**
- *Saves the current value of ({@link SearchOverManager#search}) then pushes it to a command or sign depending on how the gui was opened
+ * Saves the current value of ({@link SearchOverManager#search}) then pushes it to a command or sign depending on how the gui was opened
*/
protected static void pushSearch() {
//save to history
if (!search.isEmpty()) {
saveHistory();
}
+ //add pet level or dungeon starts if in ah
+ if (isAuction) {
+ addExtras();
+ }
+ //push
if (isCommand) {
pushCommand();
} else {
@@ -301,6 +334,45 @@ public class SearchOverManager {
}
/**
+ * Adds pet level 100 or necessary dungeon starts if needed
+ */
+ private static void addExtras() {
+ // pet level
+ if (maxPetLevel) {
+ if (auctionPets.contains(search.toLowerCase())) {
+ if (search.equalsIgnoreCase("golden dragon")) {
+ search = "[Lvl 200] " + search;
+ } else {
+ search = "[Lvl 100] " + search;
+ }
+ }
+ } else {
+ // still filter for only pets
+ if (auctionPets.contains(search.toLowerCase())) {
+ // add bracket so only get pets
+ search = "] " + search;
+ }
+ }
+
+ // dungeon stars
+ // check if it's a dungeon item and if so add correct stars
+ if (dungeonStars > 0 && starableItems.contains(search.toLowerCase())) {
+ StringBuilder starString = new StringBuilder(" ");
+ //add stars up to 5
+ starString.append("✪".repeat(Math.max(0, Math.min(dungeonStars, 5))));
+ //add number for other stars
+ switch (dungeonStars) {
+ case 6 -> starString.append("➊");
+ case 7 -> starString.append("➋");
+ case 8 -> starString.append("➌");
+ case 9 -> starString.append("➍");
+ case 10 -> starString.append("➎");
+ }
+ search += starString.toString();
+ }
+ }
+
+ /**
* runs the command to search for the value in ({@link SearchOverManager#search})
*/
private static void pushCommand() {
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/JacobsContestWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/JacobsContestWidget.java
index 24dcc229..c28c8679 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/JacobsContestWidget.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/JacobsContestWidget.java
@@ -26,7 +26,7 @@ public class JacobsContestWidget extends Widget {
//TODO Properly match the contest placement and display it
private static final Pattern CROP_PATTERN = Pattern.compile("(?<fortune>[☘○]) (?<crop>[A-Za-z ]+).*");
- private static final Map<String, ItemStack> FARM_DATA = Map.ofEntries(
+ public static final Map<String, ItemStack> FARM_DATA = Map.ofEntries(
entry("Wheat", new ItemStack(Items.WHEAT)),
entry("Sugar Cane", new ItemStack(Items.SUGAR_CANE)),
entry("Carrot", new ItemStack(Items.CARROT)),