aboutsummaryrefslogtreecommitdiff
path: root/src/main/java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java')
-rw-r--r--src/main/java/de/hysky/skyblocker/SkyblockerMod.java10
-rw-r--r--src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java3
-rw-r--r--src/main/java/de/hysky/skyblocker/config/SkyblockerConfigManager.java1
-rw-r--r--src/main/java/de/hysky/skyblocker/config/categories/EventNotificationsCategory.java68
-rw-r--r--src/main/java/de/hysky/skyblocker/config/categories/GeneralCategory.java8
-rw-r--r--src/main/java/de/hysky/skyblocker/config/configs/EventNotificationsConfig.java35
-rw-r--r--src/main/java/de/hysky/skyblocker/config/configs/GeneralConfig.java3
-rw-r--r--src/main/java/de/hysky/skyblocker/debug/Debug.java6
-rw-r--r--src/main/java/de/hysky/skyblocker/injected/SkyblockerStack.java20
-rw-r--r--src/main/java/de/hysky/skyblocker/mixins/DrawContextMixin.java52
-rw-r--r--src/main/java/de/hysky/skyblocker/mixins/HandledScreenMixin.java530
-rw-r--r--src/main/java/de/hysky/skyblocker/mixins/ItemStackMixin.java113
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/ChestValue.java66
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/TeleportOverlay.java3
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/auction/AuctionBrowserScreen.java4
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/auction/widgets/CategoryTabWidget.java28
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/ChocolateFactorySolver.java386
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/EggFinder.java37
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/TimeTowerReminder.java13
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/CroesusProfit.java28
-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/FarmingHud.java55
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/garden/FarmingHudWidget.java77
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/garden/LowerSensitivity.java17
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/garden/VisitorHelper.java52
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/slottext/SlotText.java21
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/slottext/SlotTextAdder.java58
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/slottext/SlotTextManager.java71
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/slottext/TextPosition.java8
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/AttributeShardAdder.java (renamed from src/main/java/de/hysky/skyblocker/skyblock/item/AttributeShards.java)48
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/CatacombsLevelAdder.java105
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/CollectionAdder.java33
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/CommunityShopAdder.java60
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/EnchantmentLevelAdder.java46
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/MinionLevelAdder.java34
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/PetLevelAdder.java28
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/PotionLevelAdder.java27
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/PrehistoricEggAdder.java34
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/RancherBootsSpeedAdder.java36
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/SkillLevelAdder.java34
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/SkyblockLevelAdder.java29
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/AccessoriesHelper.java4
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/CompactorDeletorPreview.java4
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ExoticTooltip.java96
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ItemTooltip.java352
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipAdder.java45
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipManager.java87
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/AccessoryTooltip.java45
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/AvgBinTooltip.java63
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/BazaarPriceTooltip.java57
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/ColorTooltip.java135
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/DungeonQualityTooltip.java59
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/LBinTooltip.java40
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/LineSmoothener.java34
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/MotesTooltip.java50
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/MuseumTooltip.java49
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/NpcPriceTooltip.java28
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/ObtainedDateTooltip.java73
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/SupercraftReminder.java32
-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/ResultButtonWidget.java30
-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
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/ItemUtils.java84
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/RomanNumerals.java54
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/Utils.java21
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/config/DurationController.java70
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/config/DurationControllerWidget.java38
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/render/gui/AbstractContainerMatcher.java29
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/render/gui/ContainerSolver.java10
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/render/gui/SideTabButtonWidget.java39
77 files changed, 3621 insertions, 1250 deletions
diff --git a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java
index bdb1766c..eff88783 100644
--- a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java
+++ b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java
@@ -2,7 +2,6 @@ package de.hysky.skyblocker;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
-import de.hysky.skyblocker.config.ImageRepoLoader;
import de.hysky.skyblocker.config.SkyblockerConfigManager;
import de.hysky.skyblocker.config.datafixer.ConfigDataFixer;
import de.hysky.skyblocker.debug.Debug;
@@ -26,15 +25,17 @@ import de.hysky.skyblocker.skyblock.end.EnderNodes;
import de.hysky.skyblocker.skyblock.end.TheEnd;
import de.hysky.skyblocker.skyblock.entity.MobBoundingBoxes;
import de.hysky.skyblocker.skyblock.fancybars.FancyStatusBars;
+import de.hysky.skyblocker.skyblock.events.EventNotifications;
import de.hysky.skyblocker.skyblock.garden.FarmingHud;
import de.hysky.skyblocker.skyblock.garden.LowerSensitivity;
import de.hysky.skyblocker.skyblock.garden.VisitorHelper;
import de.hysky.skyblocker.skyblock.item.*;
+import de.hysky.skyblocker.skyblock.item.slottext.SlotTextManager;
import de.hysky.skyblocker.skyblock.item.tooltip.AccessoriesHelper;
import de.hysky.skyblocker.skyblock.item.tooltip.BackpackPreview;
import de.hysky.skyblocker.skyblock.item.tooltip.ItemTooltip;
+import de.hysky.skyblocker.skyblock.item.tooltip.TooltipManager;
import de.hysky.skyblocker.skyblock.itemlist.ItemRepository;
-import de.hysky.skyblocker.skyblock.quicknav.QuickNav;
import de.hysky.skyblocker.skyblock.rift.TheRift;
import de.hysky.skyblocker.skyblock.searchoverlay.SearchOverManager;
import de.hysky.skyblocker.skyblock.shortcut.Shortcuts;
@@ -106,7 +107,7 @@ public class SkyblockerMod implements ClientModInitializer {
SkyblockerScreen.initClass();
Tips.init();
NEURepoManager.init();
- ImageRepoLoader.init();
+ //ImageRepoLoader.init();
ItemRepository.init();
PlayerHeadHashCache.init();
HotbarSlotLock.init();
@@ -179,6 +180,7 @@ public class SkyblockerMod implements ClientModInitializer {
Kuudra.init();
RenderHelper.init();
FancyStatusBars.init();
+ EventNotifications.init();
containerSolverManager.init();
statusBarTracker.init();
BeaconHighlighter.init();
@@ -187,6 +189,8 @@ public class SkyblockerMod implements ClientModInitializer {
EggFinder.init();
TimeTowerReminder.init();
SkyblockTime.init();
+ TooltipManager.init();
+ SlotTextManager.init();
Scheduler.INSTANCE.scheduleCyclic(Utils::update, 20);
Scheduler.INSTANCE.scheduleCyclic(DiscordRPCManager::updateDataAndPresence, 200);
diff --git a/src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java b/src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java
index 9c495382..c9246599 100644
--- a/src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java
+++ b/src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java
@@ -44,5 +44,8 @@ public class SkyblockerConfig {
public QuickNavigationConfig quickNav = new QuickNavigationConfig();
@SerialEntry
+ public EventNotificationsConfig eventNotifications = new EventNotificationsConfig();
+
+ @SerialEntry
public MiscConfig misc = new MiscConfig();
}
diff --git a/src/main/java/de/hysky/skyblocker/config/SkyblockerConfigManager.java b/src/main/java/de/hysky/skyblocker/config/SkyblockerConfigManager.java
index dd406b8a..f519473c 100644
--- a/src/main/java/de/hysky/skyblocker/config/SkyblockerConfigManager.java
+++ b/src/main/java/de/hysky/skyblocker/config/SkyblockerConfigManager.java
@@ -83,6 +83,7 @@ public class SkyblockerConfigManager {
.category(SlayersCategory.create(defaults, config))
.category(ChatCategory.create(defaults, config))
.category(QuickNavigationCategory.create(defaults, config))
+ .category(EventNotificationsCategory.create(defaults, config))
.category(MiscCategory.create(defaults, config))).generateScreen(parent);
}
diff --git a/src/main/java/de/hysky/skyblocker/config/categories/EventNotificationsCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/EventNotificationsCategory.java
new file mode 100644
index 00000000..6fd01cf8
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/config/categories/EventNotificationsCategory.java
@@ -0,0 +1,68 @@
+package de.hysky.skyblocker.config.categories;
+
+import de.hysky.skyblocker.config.ConfigUtils;
+import de.hysky.skyblocker.config.SkyblockerConfig;
+import de.hysky.skyblocker.config.configs.EventNotificationsConfig;
+import de.hysky.skyblocker.skyblock.events.EventNotifications;
+import de.hysky.skyblocker.utils.config.DurationController;
+import dev.isxander.yacl3.api.*;
+import it.unimi.dsi.fastutil.ints.IntImmutableList;
+import it.unimi.dsi.fastutil.ints.IntList;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.sound.PositionedSoundInstance;
+import net.minecraft.text.Text;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+public class EventNotificationsCategory {
+
+ private static boolean shouldPlaySound = false;
+
+ public static ConfigCategory create(SkyblockerConfig defaults, SkyblockerConfig config) {
+ shouldPlaySound = false;
+ return ConfigCategory.createBuilder()
+ .name(Text.translatable("skyblocker.config.eventNotifications"))
+ .option(Option.<EventNotificationsConfig.Sound>createBuilder()
+ .binding(defaults.eventNotifications.reminderSound,
+ () -> config.eventNotifications.reminderSound,
+ sound -> config.eventNotifications.reminderSound = sound)
+ .controller(ConfigUtils::createEnumCyclingListController)
+ .name(Text.translatable("skyblocker.config.eventNotifications.notificationSound"))
+ .listener((soundOption, sound) -> {
+ if (!shouldPlaySound) {
+ shouldPlaySound = true;
+ return;
+ }
+ if (sound.getSoundEvent() != null)
+ MinecraftClient.getInstance().getSoundManager().play(PositionedSoundInstance.master(sound.getSoundEvent(), 1f, 1f));
+ })
+ .build())
+ .groups(createGroups(config))
+ .build();
+
+ }
+
+ private static List<OptionGroup> createGroups(SkyblockerConfig config) {
+ Map<String, IntList> eventsReminderTimes = config.eventNotifications.eventsReminderTimes;
+ List<OptionGroup> groups = new ArrayList<>(eventsReminderTimes.size());
+ if (eventsReminderTimes.isEmpty()) return List.of(OptionGroup.createBuilder().option(LabelOption.create(Text.translatable("skyblocker.config.eventNotifications.monologue"))).build());
+ for (Map.Entry<String, IntList> entry : eventsReminderTimes.entrySet()) {
+ groups.add(ListOption.<Integer>createBuilder()
+ .name(Text.literal(entry.getKey()))
+ .binding(EventNotifications.DEFAULT_REMINDERS, entry::getValue, integers -> entry.setValue(new IntImmutableList(integers)))
+ .controller(option -> () -> new DurationController(option)) // yea
+ .description(OptionDescription.of(Text.translatable("skyblocker.config.eventNotifications.@Tooltip[0]"),
+ Text.empty(),
+ Text.translatable("skyblocker.config.eventNotifications.@Tooltip[1]"),
+ Text.empty(),
+ Text.translatable("skyblocker.config.eventNotifications.@Tooltip[2]", entry.getKey())))
+ .initial(60)
+ .collapsed(true)
+ .build()
+ );
+ }
+ return groups;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/config/categories/GeneralCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/GeneralCategory.java
index 1477d669..fa87be3d 100644
--- a/src/main/java/de/hysky/skyblocker/config/categories/GeneralCategory.java
+++ b/src/main/java/de/hysky/skyblocker/config/categories/GeneralCategory.java
@@ -211,6 +211,14 @@ public class GeneralCategory {
.name(Text.translatable("skyblocker.config.general.itemInfoDisplay"))
.collapsed(true)
.option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("skyblocker.config.general.itemInfoDisplay.slotText"))
+ .description(OptionDescription.of(Text.translatable("skyblocker.config.general.itemInfoDisplay.slotText.@Tooltip")))
+ .binding(defaults.general.itemInfoDisplay.slotText,
+ () -> config.general.itemInfoDisplay.slotText,
+ newValue -> config.general.itemInfoDisplay.slotText = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
.name(Text.translatable("skyblocker.config.general.itemInfoDisplay.attributeShardInfo"))
.description(OptionDescription.of(Text.translatable("skyblocker.config.general.itemInfoDisplay.attributeShardInfo.@Tooltip")))
.binding(defaults.general.itemInfoDisplay.attributeShardInfo,
diff --git a/src/main/java/de/hysky/skyblocker/config/configs/EventNotificationsConfig.java b/src/main/java/de/hysky/skyblocker/config/configs/EventNotificationsConfig.java
new file mode 100644
index 00000000..c43ae7a6
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/config/configs/EventNotificationsConfig.java
@@ -0,0 +1,35 @@
+package de.hysky.skyblocker.config.configs;
+
+import dev.isxander.yacl3.config.v2.api.SerialEntry;
+import it.unimi.dsi.fastutil.ints.IntList;
+import net.minecraft.sound.SoundEvent;
+import net.minecraft.sound.SoundEvents;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class EventNotificationsConfig {
+
+ @SerialEntry
+ public Sound reminderSound = Sound.PLING;
+
+ @SerialEntry
+ public Map<String, IntList> eventsReminderTimes = new HashMap<>();
+
+ public enum Sound {
+ NONE(null),
+ BELL(SoundEvents.BLOCK_BELL_USE),
+ DING(SoundEvents.ENTITY_ARROW_HIT_PLAYER),
+ PLING(SoundEvents.BLOCK_NOTE_BLOCK_PLING.value()),
+ GOAT(SoundEvents.GOAT_HORN_SOUNDS.getFirst().value());
+
+ public SoundEvent getSoundEvent() {
+ return soundEvent;
+ }
+
+ final SoundEvent soundEvent;
+ Sound(SoundEvent soundEvent) {
+ this.soundEvent = soundEvent;
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/config/configs/GeneralConfig.java b/src/main/java/de/hysky/skyblocker/config/configs/GeneralConfig.java
index 56e110b6..aa7566a0 100644
--- a/src/main/java/de/hysky/skyblocker/config/configs/GeneralConfig.java
+++ b/src/main/java/de/hysky/skyblocker/config/configs/GeneralConfig.java
@@ -141,6 +141,9 @@ public class GeneralConfig {
public static class ItemInfoDisplay {
@SerialEntry
+ public boolean slotText = true;
+
+ @SerialEntry
public boolean attributeShardInfo = true;
@SerialEntry
diff --git a/src/main/java/de/hysky/skyblocker/debug/Debug.java b/src/main/java/de/hysky/skyblocker/debug/Debug.java
index d642ca5b..16d91635 100644
--- a/src/main/java/de/hysky/skyblocker/debug/Debug.java
+++ b/src/main/java/de/hysky/skyblocker/debug/Debug.java
@@ -16,6 +16,7 @@ import net.minecraft.client.gui.screen.ingame.HandledScreen;
import net.minecraft.entity.decoration.ArmorStandEntity;
import net.minecraft.item.ItemStack;
import net.minecraft.predicate.entity.EntityPredicates;
+import net.minecraft.screen.slot.Slot;
import net.minecraft.text.Text;
import org.lwjgl.glfw.GLFW;
@@ -48,8 +49,9 @@ public class Debug {
ScreenEvents.BEFORE_INIT.register((client, screen, scaledWidth, scaledHeight) -> {
if (screen instanceof HandledScreen<?> handledScreen) {
ScreenKeyboardEvents.afterKeyPress(screen).register((_screen, key, scancode, modifier) -> {
- if (key == GLFW.GLFW_KEY_U && client.player != null) {
- client.player.sendMessage(Text.literal("[Skyblocker Debug] Hovered Item: " + SkyblockerMod.GSON_COMPACT.toJson(ItemStack.CODEC.encodeStart(JsonOps.INSTANCE, ((HandledScreenAccessor) handledScreen).getFocusedSlot().getStack()))));
+ Slot focusedSlot = ((HandledScreenAccessor) handledScreen).getFocusedSlot();
+ if (key == GLFW.GLFW_KEY_U && client.player != null && focusedSlot != null && focusedSlot.hasStack()) {
+ client.player.sendMessage(Text.literal("[Skyblocker Debug] Hovered Item: " + SkyblockerMod.GSON_COMPACT.toJson(ItemStack.CODEC.encodeStart(JsonOps.INSTANCE, focusedSlot.getStack()))));
}
});
}
diff --git a/src/main/java/de/hysky/skyblocker/injected/SkyblockerStack.java b/src/main/java/de/hysky/skyblocker/injected/SkyblockerStack.java
new file mode 100644
index 00000000..2f54917b
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/injected/SkyblockerStack.java
@@ -0,0 +1,20 @@
+package de.hysky.skyblocker.injected;
+
+import org.jetbrains.annotations.Nullable;
+
+public interface SkyblockerStack {
+ @Nullable
+ default String getSkyblockId() {
+ return "";
+ }
+
+ @Nullable
+ default String getSkyblockApiId() {
+ return "";
+ }
+
+ @Nullable
+ default String getNeuName() {
+ return "";
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/mixins/DrawContextMixin.java b/src/main/java/de/hysky/skyblocker/mixins/DrawContextMixin.java
index 7964b114..9f09c37a 100644
--- a/src/main/java/de/hysky/skyblocker/mixins/DrawContextMixin.java
+++ b/src/main/java/de/hysky/skyblocker/mixins/DrawContextMixin.java
@@ -2,67 +2,15 @@ package de.hysky.skyblocker.mixins;
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
import com.llamalad7.mixinextras.sugar.Local;
-import com.llamalad7.mixinextras.sugar.ref.LocalRef;
-import de.hysky.skyblocker.config.SkyblockerConfigManager;
-import de.hysky.skyblocker.skyblock.item.AttributeShards;
import de.hysky.skyblocker.skyblock.item.ItemCooldowns;
-import de.hysky.skyblocker.utils.ItemUtils;
import de.hysky.skyblocker.utils.Utils;
-import net.minecraft.client.font.TextRenderer;
import net.minecraft.client.gui.DrawContext;
-import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.item.ItemStack;
-import net.minecraft.nbt.NbtCompound;
-import net.minecraft.util.Formatting;
-import org.jetbrains.annotations.Nullable;
-import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
-import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
-import org.spongepowered.asm.mixin.injection.Inject;
-import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(DrawContext.class)
public abstract class DrawContextMixin {
- @Shadow
- @Final
- private MatrixStack matrices;
-
- @Shadow
- public abstract int drawText(TextRenderer textRenderer, @Nullable String text, int x, int y, int color, boolean shadow);
-
- @Inject(method = "drawItemInSlot(Lnet/minecraft/client/font/TextRenderer;Lnet/minecraft/item/ItemStack;IILjava/lang/String;)V", at = @At("HEAD"))
- private void skyblocker$renderAttributeShardDisplay(CallbackInfo ci, @Local(argsOnly = true) TextRenderer textRenderer, @Local(argsOnly = true) ItemStack stack, @Local(argsOnly = true, ordinal = 0) int x, @Local(argsOnly = true, ordinal = 1) int y, @Local(argsOnly = true) LocalRef<String> countOverride) {
- if (!SkyblockerConfigManager.get().general.itemInfoDisplay.attributeShardInfo) return;
-
- if (Utils.isOnSkyblock()) {
- NbtCompound customData = ItemUtils.getCustomData(stack);
-
- if (ItemUtils.getItemId(stack).equals("ATTRIBUTE_SHARD")) {
- NbtCompound attributesTag = customData.getCompound("attributes");
- String[] attributes = attributesTag.getKeys().toArray(String[]::new);
-
- if (attributes.length != 0) {
- String attributeId = attributes[0];
- int attributeLevel = attributesTag.getInt(attributeId);
-
- //Set item count
- countOverride.set(Integer.toString(attributeLevel));
-
- //Draw the attribute name
- this.matrices.push();
- this.matrices.translate(0f, 0f, 200f);
-
- String attributeInitials = AttributeShards.getShortName(attributeId);
-
- this.drawText(textRenderer, attributeInitials, x, y, Formatting.AQUA.getColorValue(), true);
-
- this.matrices.pop();
- }
- }
- }
- }
-
@ModifyExpressionValue(method = "drawItemInSlot(Lnet/minecraft/client/font/TextRenderer;Lnet/minecraft/item/ItemStack;IILjava/lang/String;)V",
at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/player/ItemCooldownManager;getCooldownProgress(Lnet/minecraft/item/Item;F)F"))
private float skyblocker$modifyItemCooldown(float cooldownProgress, @Local(argsOnly = true) ItemStack stack) {
diff --git a/src/main/java/de/hysky/skyblocker/mixins/HandledScreenMixin.java b/src/main/java/de/hysky/skyblocker/mixins/HandledScreenMixin.java
index f662be7c..1a97c471 100644
--- a/src/main/java/de/hysky/skyblocker/mixins/HandledScreenMixin.java
+++ b/src/main/java/de/hysky/skyblocker/mixins/HandledScreenMixin.java
@@ -12,6 +12,8 @@ import de.hysky.skyblocker.skyblock.garden.VisitorHelper;
import de.hysky.skyblocker.skyblock.item.ItemProtection;
import de.hysky.skyblocker.skyblock.item.ItemRarityBackgrounds;
import de.hysky.skyblocker.skyblock.item.WikiLookup;
+import de.hysky.skyblocker.skyblock.item.slottext.SlotText;
+import de.hysky.skyblocker.skyblock.item.slottext.SlotTextManager;
import de.hysky.skyblocker.skyblock.item.tooltip.BackpackPreview;
import de.hysky.skyblocker.skyblock.item.tooltip.CompactorDeletorPreview;
import de.hysky.skyblocker.skyblock.quicknav.QuickNav;
@@ -22,6 +24,7 @@ import de.hysky.skyblocker.utils.render.gui.ContainerSolver;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.screen.Screen;
import net.minecraft.client.gui.screen.ingame.HandledScreen;
+import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.inventory.SimpleInventory;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
@@ -50,251 +53,284 @@ import java.util.regex.Matcher;
@Mixin(HandledScreen.class)
public abstract class HandledScreenMixin<T extends ScreenHandler> extends Screen {
- /**
- * This is the slot id returned for when a click is outside the screen's bounds
- */
- @Unique
- private static final int OUT_OF_BOUNDS_SLOT = -999;
-
- @Unique
- private static final Identifier ITEM_PROTECTION = new Identifier(SkyblockerMod.NAMESPACE, "textures/gui/item_protection.png");
-
- @Unique
- private static final Set<String> FILLER_ITEMS = Set.of(
- " ", // Empty menu item
- "Locked Page",
- "Quick Crafting Slot",
- "Locked Backpack Slot 2", //Regular expressions won't be utilized here since the search by contains is based on plain text rather than regex syntax
- "Locked Backpack Slot 3",
- "Locked Backpack Slot 4",
- "Locked Backpack Slot 5",
- "Locked Backpack Slot 6",
- "Locked Backpack Slot 7",
- "Locked Backpack Slot 8",
- "Locked Backpack Slot 9",
- "Locked Backpack Slot 10",
- "Locked Backpack Slot 11",
- "Locked Backpack Slot 12",
- "Locked Backpack Slot 13",
- "Locked Backpack Slot 14",
- "Locked Backpack Slot 15",
- "Locked Backpack Slot 16",
- "Locked Backpack Slot 17",
- "Locked Backpack Slot 18",
- "Preparing"
- );
-
- @Shadow
- @Nullable
- protected Slot focusedSlot;
-
- @Shadow
- @Final
- protected T handler;
-
- @Unique
- private List<QuickNavButton> quickNavButtons;
-
- protected HandledScreenMixin(Text title) {
- super(title);
- }
-
- @Inject(method = "init", at = @At("RETURN"))
- private void skyblocker$initQuickNav(CallbackInfo ci) {
- if (Utils.isOnSkyblock() && SkyblockerConfigManager.get().quickNav.enableQuickNav && client != null && client.player != null && !client.player.isCreative()) {
- for (QuickNavButton quickNavButton : quickNavButtons = QuickNav.init(getTitle().getString().trim())) {
- addSelectableChild(quickNavButton);
- }
- }
- }
-
- @Inject(at = @At("HEAD"), method = "keyPressed")
- public void skyblocker$keyPressed(int keyCode, int scanCode, int modifiers, CallbackInfoReturnable<Boolean> cir) {
- if (this.client != null && this.focusedSlot != null && keyCode != 256) {
- //wiki lookup
- if (!this.client.options.inventoryKey.matchesKey(keyCode, scanCode) && WikiLookup.wikiLookup.matchesKey(keyCode, scanCode) && client.player != null) {
- WikiLookup.openWiki(this.focusedSlot, client.player);
- }
- //item protection
- if (!this.client.options.inventoryKey.matchesKey(keyCode, scanCode) && ItemProtection.itemProtection.matchesKey(keyCode, scanCode)) {
- ItemProtection.handleKeyPressed(this.focusedSlot.getStack());
- }
- }
- }
-
- @Inject(at = @At("HEAD"), method = "mouseClicked")
- public void skyblocker$mouseClicked(double mouseX, double mouseY, int button, CallbackInfoReturnable<Boolean> cir) {
- if (SkyblockerConfigManager.get().farming.garden.visitorHelper && (Utils.getLocationRaw().equals("garden") && !getTitle().getString().contains("Logbook") || getTitle().getString().startsWith("Bazaar"))) {
- VisitorHelper.onMouseClicked(mouseX, mouseY, button, this.textRenderer);
- }
- }
-
- /**
- * Draws the unselected tabs in front of the background blur, but behind the main inventory, similar to creative inventory tabs
- */
- @Inject(method = "renderBackground", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screen/ingame/HandledScreen;drawBackground(Lnet/minecraft/client/gui/DrawContext;FII)V"))
- private void skyblocker$drawUnselectedQuickNavButtons(DrawContext context, int mouseX, int mouseY, float delta, CallbackInfo ci) {
- if (quickNavButtons != null) for (QuickNavButton quickNavButton : quickNavButtons) {
- if (!quickNavButton.toggled()) {
- quickNavButton.render(context, mouseX, mouseY, delta);
- }
- }
- }
-
- /**
- * Draws the selected tab in front of the background blur and the main inventory, similar to creative inventory tabs
- */
- @Inject(method = "renderBackground", at = @At("RETURN"))
- private void skyblocker$drawSelectedQuickNavButtons(DrawContext context, int mouseX, int mouseY, float delta, CallbackInfo ci) {
- if (quickNavButtons != null) for (QuickNavButton quickNavButton : quickNavButtons) {
- if (quickNavButton.toggled()) {
- quickNavButton.render(context, mouseX, mouseY, delta);
- }
- }
- }
-
- @SuppressWarnings("DataFlowIssue")
- // makes intellij be quiet about this.focusedSlot maybe being null. It's already null checked in mixined method.
- @Inject(method = "drawMouseoverTooltip", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/DrawContext;drawTooltip(Lnet/minecraft/client/font/TextRenderer;Ljava/util/List;Ljava/util/Optional;II)V"), cancellable = true)
- public void skyblocker$drawMouseOverTooltip(DrawContext context, int x, int y, CallbackInfo ci, @Local(ordinal = 0) ItemStack stack) {
- if (!Utils.isOnSkyblock()) return;
-
- // Hide Empty Tooltips
- if (SkyblockerConfigManager.get().uiAndVisuals.hideEmptyTooltips && stack.getName().getString().equals(" ")) {
- ci.cancel();
- }
-
- // Backpack Preview
- boolean shiftDown = SkyblockerConfigManager.get().uiAndVisuals.backpackPreviewWithoutShift ^ Screen.hasShiftDown();
- if (shiftDown && getTitle().getString().equals("Storage") && focusedSlot.inventory != client.player.getInventory() && BackpackPreview.renderPreview(context, this, focusedSlot.getIndex(), x, y)) {
- ci.cancel();
- }
-
- // Compactor Preview
- if (SkyblockerConfigManager.get().uiAndVisuals.compactorDeletorPreview) {
- Matcher matcher = CompactorDeletorPreview.NAME.matcher(ItemUtils.getItemId(stack));
- if (matcher.matches() && CompactorDeletorPreview.drawPreview(context, stack, matcher.group("type"), matcher.group("size"), x, y)) {
- ci.cancel();
- }
- }
- }
-
- @ModifyVariable(method = "drawMouseoverTooltip", at = @At(value = "LOAD", ordinal = 0))
- private ItemStack skyblocker$experimentSolvers$replaceTooltipDisplayStack(ItemStack stack) {
- return skyblocker$experimentSolvers$getStack(focusedSlot, stack);
- }
-
- @ModifyVariable(method = "drawSlot", at = @At(value = "LOAD", ordinal = 3), ordinal = 0)
- private ItemStack skyblocker$experimentSolvers$replaceDisplayStack(ItemStack stack, DrawContext context, Slot slot) {
- return skyblocker$experimentSolvers$getStack(slot, stack);
- }
-
- /**
- * Redirects getStack calls to account for different stacks in experiment solvers.
- */
- @Unique
- private ItemStack skyblocker$experimentSolvers$getStack(Slot slot, @NotNull ItemStack stack) {
- ContainerSolver currentSolver = SkyblockerMod.getInstance().containerSolverManager.getCurrentSolver();
- if ((currentSolver instanceof SuperpairsSolver || currentSolver instanceof UltrasequencerSolver) && ((ExperimentSolver) currentSolver).getState() == ExperimentSolver.State.SHOW && slot.inventory instanceof SimpleInventory) {
- ItemStack itemStack = ((ExperimentSolver) currentSolver).getSlots().get(slot.getIndex());
- return itemStack == null ? stack : itemStack;
- }
- return stack;
- }
-
- /**
- * The naming of this method in yarn is half true, its mostly to handle slot/item interactions (which are mouse or keyboard clicks)
- * For example, using the drop key bind while hovering over an item will invoke this method to drop the players item
- *
- * @implNote This runs before {@link ScreenHandler#onSlotClick(int, int, SlotActionType, net.minecraft.entity.player.PlayerEntity)}
- */
- @Inject(method = "onMouseClick(Lnet/minecraft/screen/slot/Slot;IILnet/minecraft/screen/slot/SlotActionType;)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/network/ClientPlayerInteractionManager;clickSlot(IIILnet/minecraft/screen/slot/SlotActionType;Lnet/minecraft/entity/player/PlayerEntity;)V"), cancellable = true)
- private void skyblocker$onSlotClick(Slot slot, int slotId, int button, SlotActionType actionType, CallbackInfo ci) {
- if (!Utils.isOnSkyblock()) return;
-
- // Item Protection
- // When you try and drop the item by picking it up then clicking outside the screen
- if (slotId == OUT_OF_BOUNDS_SLOT && ItemProtection.isItemProtected(this.handler.getCursorStack())) {
- ci.cancel();
- return;
- }
-
- if (slot == null) return;
- String title = getTitle().getString();
- ItemStack stack = skyblocker$experimentSolvers$getStack(slot, slot.getStack());
- ContainerSolver currentSolver = SkyblockerMod.getInstance().containerSolverManager.getCurrentSolver();
-
- // Prevent clicks on filler items
- if (SkyblockerConfigManager.get().uiAndVisuals.hideEmptyTooltips && FILLER_ITEMS.contains(stack.getName().getString()) &&
- // Allow clicks in Ultrasequencer and Superpairs
- (!UltrasequencerSolver.INSTANCE.getName().matcher(title).matches() || SkyblockerConfigManager.get().helpers.experiments.enableUltrasequencerSolver)) {
- ci.cancel();
- return;
- }
- // Item Protection
- // When you click your drop key while hovering over an item
- if (actionType == SlotActionType.THROW && ItemProtection.isItemProtected(stack)) {
- ci.cancel();
- return;
- }
- // Prevent salvaging
- if (title.equals("Salvage Items") && ItemProtection.isItemProtected(stack)) {
- ci.cancel();
- return;
- }
- if (this.handler instanceof GenericContainerScreenHandler genericContainerScreenHandler && genericContainerScreenHandler.getRows() == 6) {
- VisitorHelper.onSlotClick(slot, slotId, title, genericContainerScreenHandler.getSlot(13).getStack());
-
- // Prevent selling to NPC shops
- ItemStack sellStack = this.handler.slots.get(49).getStack();
- if (sellStack.getName().getString().equals("Sell Item") || ItemUtils.getLoreLineIf(sellStack, text -> text.contains("buyback")) != null) {
- if (slotId != 49 && ItemProtection.isItemProtected(stack)) {
- ci.cancel();
- return;
- }
- }
- }
-
- if (currentSolver != null) {
- boolean disallowed = SkyblockerMod.getInstance().containerSolverManager.onSlotClick(slotId, stack);
-
- if (disallowed) ci.cancel();
- }
-
- // Experiment Solvers
- if (currentSolver instanceof ExperimentSolver experimentSolver && experimentSolver.getState() == ExperimentSolver.State.SHOW && slot.inventory instanceof SimpleInventory) {
- switch (experimentSolver) {
- case ChronomatronSolver chronomatronSolver -> {
- Item item = chronomatronSolver.getChronomatronSlots().get(chronomatronSolver.getChronomatronCurrentOrdinal());
- if ((stack.isOf(item) || ChronomatronSolver.TERRACOTTA_TO_GLASS.get(stack.getItem()) == item) && chronomatronSolver.incrementChronomatronCurrentOrdinal() >= chronomatronSolver.getChronomatronSlots().size()) {
- chronomatronSolver.setState(ExperimentSolver.State.END);
- }
- }
-
- case SuperpairsSolver superpairsSolver -> {
- superpairsSolver.setSuperpairsPrevClickedSlot(slot.getIndex());
- superpairsSolver.setSuperpairsCurrentSlot(ItemStack.EMPTY);
- }
-
- case UltrasequencerSolver ultrasequencerSolver when slot.getIndex() == ultrasequencerSolver.getUltrasequencerNextSlot() -> {
- int count = ultrasequencerSolver.getSlots().get(ultrasequencerSolver.getUltrasequencerNextSlot()).getCount() + 1;
- ultrasequencerSolver.getSlots().entrySet().stream().filter(entry -> entry.getValue().getCount() == count).findAny().map(Map.Entry::getKey).ifPresentOrElse(ultrasequencerSolver::setUltrasequencerNextSlot, () -> ultrasequencerSolver.setState(ExperimentSolver.State.END));
- }
-
- default -> { /*Do Nothing*/ }
- }
- }
- }
-
- @Inject(method = "drawSlot", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/DrawContext;drawItem(Lnet/minecraft/item/ItemStack;III)V"))
- private void skyblocker$drawItemRarityBackground(DrawContext context, Slot slot, CallbackInfo ci) {
- if (Utils.isOnSkyblock() && SkyblockerConfigManager.get().general.itemInfoDisplay.itemRarityBackgrounds)
- ItemRarityBackgrounds.tryDraw(slot.getStack(), context, slot.x, slot.y);
- // Item protection
- if (ItemProtection.isItemProtected(slot.getStack())) {
- RenderSystem.enableBlend();
- context.drawTexture(ITEM_PROTECTION, slot.x, slot.y, 0, 0, 16, 16, 16, 16);
- RenderSystem.disableBlend();
- }
- }
+ /**
+ * This is the slot id returned for when a click is outside the screen's bounds
+ */
+ @Unique
+ private static final int OUT_OF_BOUNDS_SLOT = -999;
+
+ @Unique
+ private static final Identifier ITEM_PROTECTION = new Identifier(SkyblockerMod.NAMESPACE, "textures/gui/item_protection.png");
+
+ @Unique
+ private static final Set<String> FILLER_ITEMS = Set.of(
+ " ", // Empty menu item
+ "Locked Page",
+ "Quick Crafting Slot",
+ "Locked Backpack Slot 2", //Regular expressions won't be utilized here since the search by contains is based on plain text rather than regex syntax
+ "Locked Backpack Slot 3",
+ "Locked Backpack Slot 4",
+ "Locked Backpack Slot 5",
+ "Locked Backpack Slot 6",
+ "Locked Backpack Slot 7",
+ "Locked Backpack Slot 8",
+ "Locked Backpack Slot 9",
+ "Locked Backpack Slot 10",
+ "Locked Backpack Slot 11",
+ "Locked Backpack Slot 12",
+ "Locked Backpack Slot 13",
+ "Locked Backpack Slot 14",
+ "Locked Backpack Slot 15",
+ "Locked Backpack Slot 16",
+ "Locked Backpack Slot 17",
+ "Locked Backpack Slot 18",
+ "Preparing"
+ );
+
+ @Shadow
+ @Nullable
+ protected Slot focusedSlot;
+
+ @Shadow
+ @Final
+ protected T handler;
+
+ @Shadow
+ protected abstract List<Text> getTooltipFromItem(ItemStack stack);
+
+ @Unique
+ private List<QuickNavButton> quickNavButtons;
+
+ protected HandledScreenMixin(Text title) {
+ super(title);
+ }
+
+ @Inject(method = "init", at = @At("RETURN"))
+ private void skyblocker$initQuickNav(CallbackInfo ci) {
+ if (Utils.isOnSkyblock() && SkyblockerConfigManager.get().quickNav.enableQuickNav && client != null && client.player != null && !client.player.isCreative()) {
+ for (QuickNavButton quickNavButton : quickNavButtons = QuickNav.init(getTitle().getString().trim())) {
+ addSelectableChild(quickNavButton);
+ }
+ }
+ }
+
+ @Inject(at = @At("HEAD"), method = "keyPressed")
+ public void skyblocker$keyPressed(int keyCode, int scanCode, int modifiers, CallbackInfoReturnable<Boolean> cir) {
+ if (this.client != null && this.focusedSlot != null && keyCode != 256) {
+ //wiki lookup
+ if (!this.client.options.inventoryKey.matchesKey(keyCode, scanCode) && WikiLookup.wikiLookup.matchesKey(keyCode, scanCode) && client.player != null) {
+ WikiLookup.openWiki(this.focusedSlot, client.player);
+ }
+ //item protection
+ if (!this.client.options.inventoryKey.matchesKey(keyCode, scanCode) && ItemProtection.itemProtection.matchesKey(keyCode, scanCode)) {
+ ItemProtection.handleKeyPressed(this.focusedSlot.getStack());
+ }
+ }
+ }
+
+ @Inject(at = @At("HEAD"), method = "mouseClicked")
+ public void skyblocker$mouseClicked(double mouseX, double mouseY, int button, CallbackInfoReturnable<Boolean> cir) {
+ if (SkyblockerConfigManager.get().farming.garden.visitorHelper && (Utils.getLocationRaw().equals("garden") && !getTitle().getString().contains("Logbook") || getTitle().getString().startsWith("Bazaar"))) {
+ VisitorHelper.onMouseClicked(mouseX, mouseY, button, this.textRenderer);
+ }
+ }
+
+ /**
+ * Draws the unselected tabs in front of the background blur, but behind the main inventory, similar to creative inventory tabs
+ */
+ @Inject(method = "renderBackground", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screen/ingame/HandledScreen;drawBackground(Lnet/minecraft/client/gui/DrawContext;FII)V"))
+ private void skyblocker$drawUnselectedQuickNavButtons(DrawContext context, int mouseX, int mouseY, float delta, CallbackInfo ci) {
+ if (quickNavButtons != null) for (QuickNavButton quickNavButton : quickNavButtons) {
+ if (!quickNavButton.toggled()) {
+ quickNavButton.render(context, mouseX, mouseY, delta);
+ }
+ }
+ }
+
+ /**
+ * Draws the selected tab in front of the background blur and the main inventory, similar to creative inventory tabs
+ */
+ @Inject(method = "renderBackground", at = @At("RETURN"))
+ private void skyblocker$drawSelectedQuickNavButtons(DrawContext context, int mouseX, int mouseY, float delta, CallbackInfo ci) {
+ if (quickNavButtons != null) for (QuickNavButton quickNavButton : quickNavButtons) {
+ if (quickNavButton.toggled()) {
+ quickNavButton.render(context, mouseX, mouseY, delta);
+ }
+ }
+ }
+
+ @SuppressWarnings("DataFlowIssue")
+ // makes intellij be quiet about this.focusedSlot maybe being null. It's already null checked in mixined method.
+ @Inject(method = "drawMouseoverTooltip", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/DrawContext;drawTooltip(Lnet/minecraft/client/font/TextRenderer;Ljava/util/List;Ljava/util/Optional;II)V"), cancellable = true)
+ public void skyblocker$drawMouseOverTooltip(DrawContext context, int x, int y, CallbackInfo ci, @Local(ordinal = 0) ItemStack stack) {
+ if (!Utils.isOnSkyblock()) return;
+
+ // Hide Empty Tooltips
+ if (SkyblockerConfigManager.get().uiAndVisuals.hideEmptyTooltips && stack.getName().getString().equals(" ")) {
+ ci.cancel();
+ }
+
+ // Backpack Preview
+ boolean shiftDown = SkyblockerConfigManager.get().uiAndVisuals.backpackPreviewWithoutShift ^ Screen.hasShiftDown();
+ if (shiftDown && getTitle().getString().equals("Storage") && focusedSlot.inventory != client.player.getInventory() && BackpackPreview.renderPreview(context, this, focusedSlot.getIndex(), x, y)) {
+ ci.cancel();
+ }
+
+ // Compactor Preview
+ if (SkyblockerConfigManager.get().uiAndVisuals.compactorDeletorPreview) {
+ Matcher matcher = CompactorDeletorPreview.NAME.matcher(ItemUtils.getItemId(stack));
+ if (matcher.matches() && CompactorDeletorPreview.drawPreview(context, stack, getTooltipFromItem(stack), matcher.group("type"), matcher.group("size"), x, y)) {
+ ci.cancel();
+ }
+ }
+ }
+
+ @ModifyVariable(method = "drawMouseoverTooltip", at = @At(value = "LOAD", ordinal = 0))
+ private ItemStack skyblocker$experimentSolvers$replaceTooltipDisplayStack(ItemStack stack) {
+ return skyblocker$experimentSolvers$getStack(focusedSlot, stack);
+ }
+
+ @ModifyVariable(method = "drawSlot", at = @At(value = "LOAD", ordinal = 3), ordinal = 0)
+ private ItemStack skyblocker$experimentSolvers$replaceDisplayStack(ItemStack stack, DrawContext context, Slot slot) {
+ return skyblocker$experimentSolvers$getStack(slot, stack);
+ }
+
+ /**
+ * Redirects getStack calls to account for different stacks in experiment solvers.
+ */
+ @Unique
+ private ItemStack skyblocker$experimentSolvers$getStack(Slot slot, @NotNull ItemStack stack) {
+ ContainerSolver currentSolver = SkyblockerMod.getInstance().containerSolverManager.getCurrentSolver();
+ if ((currentSolver instanceof SuperpairsSolver || currentSolver instanceof UltrasequencerSolver) && ((ExperimentSolver) currentSolver).getState() == ExperimentSolver.State.SHOW && slot.inventory instanceof SimpleInventory) {
+ ItemStack itemStack = ((ExperimentSolver) currentSolver).getSlots().get(slot.getIndex());
+ return itemStack == null ? stack : itemStack;
+ }
+ return stack;
+ }
+
+ /**
+ * The naming of this method in yarn is half true, its mostly to handle slot/item interactions (which are mouse or keyboard clicks)
+ * For example, using the drop key bind while hovering over an item will invoke this method to drop the players item
+ *
+ * @implNote This runs before {@link ScreenHandler#onSlotClick(int, int, SlotActionType, net.minecraft.entity.player.PlayerEntity)}
+ */
+ @Inject(method = "onMouseClick(Lnet/minecraft/screen/slot/Slot;IILnet/minecraft/screen/slot/SlotActionType;)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/network/ClientPlayerInteractionManager;clickSlot(IIILnet/minecraft/screen/slot/SlotActionType;Lnet/minecraft/entity/player/PlayerEntity;)V"), cancellable = true)
+ private void skyblocker$onSlotClick(Slot slot, int slotId, int button, SlotActionType actionType, CallbackInfo ci) {
+ if (!Utils.isOnSkyblock()) return;
+
+ // Item Protection
+ // When you try and drop the item by picking it up then clicking outside the screen
+ if (slotId == OUT_OF_BOUNDS_SLOT && ItemProtection.isItemProtected(this.handler.getCursorStack())) {
+ ci.cancel();
+ return;
+ }
+
+ if (slot == null) return;
+ String title = getTitle().getString();
+ ItemStack stack = skyblocker$experimentSolvers$getStack(slot, slot.getStack());
+ ContainerSolver currentSolver = SkyblockerMod.getInstance().containerSolverManager.getCurrentSolver();
+
+ // Prevent clicks on filler items
+ if (SkyblockerConfigManager.get().uiAndVisuals.hideEmptyTooltips && FILLER_ITEMS.contains(stack.getName().getString()) &&
+ // Allow clicks in Ultrasequencer and Superpairs
+ (!UltrasequencerSolver.INSTANCE.getName().matcher(title).matches() || SkyblockerConfigManager.get().helpers.experiments.enableUltrasequencerSolver)) {
+ ci.cancel();
+ return;
+ }
+ // Item Protection
+ // When you click your drop key while hovering over an item
+ if (actionType == SlotActionType.THROW && ItemProtection.isItemProtected(stack)) {
+ ci.cancel();
+ return;
+ }
+ // Prevent salvaging
+ if (title.equals("Salvage Items") && ItemProtection.isItemProtected(stack)) {
+ ci.cancel();
+ return;
+ }
+ if (this.handler instanceof GenericContainerScreenHandler genericContainerScreenHandler && genericContainerScreenHandler.getRows() == 6) {
+ VisitorHelper.onSlotClick(slot, slotId, title, genericContainerScreenHandler.getSlot(13).getStack());
+
+ // Prevent selling to NPC shops
+ ItemStack sellStack = this.handler.slots.get(49).getStack();
+ if (sellStack.getName().getString().equals("Sell Item") || ItemUtils.getLoreLineIf(sellStack, text -> text.contains("buyback")) != null) {
+ if (slotId != 49 && ItemProtection.isItemProtected(stack)) {
+ ci.cancel();
+ return;
+ }
+ }
+ }
+
+ if (currentSolver != null) {
+ boolean disallowed = SkyblockerMod.getInstance().containerSolverManager.onSlotClick(slotId, stack);
+
+ if (disallowed) ci.cancel();
+ }
+
+ // Experiment Solvers
+ if (currentSolver instanceof ExperimentSolver experimentSolver && experimentSolver.getState() == ExperimentSolver.State.SHOW && slot.inventory instanceof SimpleInventory) {
+ switch (experimentSolver) {
+ case ChronomatronSolver chronomatronSolver -> {
+ Item item = chronomatronSolver.getChronomatronSlots().get(chronomatronSolver.getChronomatronCurrentOrdinal());
+ if ((stack.isOf(item) || ChronomatronSolver.TERRACOTTA_TO_GLASS.get(stack.getItem()) == item) && chronomatronSolver.incrementChronomatronCurrentOrdinal() >= chronomatronSolver.getChronomatronSlots().size()) {
+ chronomatronSolver.setState(ExperimentSolver.State.END);
+ }
+ }
+
+ case SuperpairsSolver superpairsSolver -> {
+ superpairsSolver.setSuperpairsPrevClickedSlot(slot.getIndex());
+ superpairsSolver.setSuperpairsCurrentSlot(ItemStack.EMPTY);
+ }
+
+ case UltrasequencerSolver ultrasequencerSolver when slot.getIndex() == ultrasequencerSolver.getUltrasequencerNextSlot() -> {
+ int count = ultrasequencerSolver.getSlots().get(ultrasequencerSolver.getUltrasequencerNextSlot()).getCount() + 1;
+ ultrasequencerSolver.getSlots().entrySet().stream().filter(entry -> entry.getValue().getCount() == count).findAny().map(Map.Entry::getKey).ifPresentOrElse(ultrasequencerSolver::setUltrasequencerNextSlot, () -> ultrasequencerSolver.setState(ExperimentSolver.State.END));
+ }
+
+ default -> { /*Do Nothing*/ }
+ }
+ }
+ }
+
+ @Inject(method = "drawSlot", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/DrawContext;drawItem(Lnet/minecraft/item/ItemStack;III)V"))
+ private void skyblocker$drawItemRarityBackground(DrawContext context, Slot slot, CallbackInfo ci) {
+ if (Utils.isOnSkyblock() && SkyblockerConfigManager.get().general.itemInfoDisplay.itemRarityBackgrounds)
+ ItemRarityBackgrounds.tryDraw(slot.getStack(), context, slot.x, slot.y);
+ // Item protection
+ if (ItemProtection.isItemProtected(slot.getStack())) {
+ RenderSystem.enableBlend();
+ context.drawTexture(ITEM_PROTECTION, slot.x, slot.y, 0, 0, 16, 16, 16, 16);
+ RenderSystem.disableBlend();
+ }
+ }
+
+ @Inject(method = "drawSlot", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/DrawContext;drawItemInSlot(Lnet/minecraft/client/font/TextRenderer;Lnet/minecraft/item/ItemStack;IILjava/lang/String;)V"))
+ private void skyblocker$drawSlotText(DrawContext context, Slot slot, CallbackInfo ci) {
+ List<SlotText> textList = SlotTextManager.getText(slot);
+ if (textList.isEmpty()) return;
+ MatrixStack matrices = context.getMatrices();
+
+ for (SlotText slotText : textList) {
+ matrices.push();
+ matrices.translate(0.0f, 0.0f, 200.0f);
+ int length = textRenderer.getWidth(slotText.text());
+ if (length > 16) {
+ matrices.scale(16.0f / length, 16.0f / length, 1.0f); //Make them fit in the slot. FYI, a slot is sized 16×16.
+ final float x = (slot.x * length / 16.0f) - slot.x; //Save in a variable to not recalculate
+ switch (slotText.position()) {
+ case TOP_LEFT, TOP_RIGHT -> matrices.translate(x, (slot.y * length / 16.0f) - slot.y, 0.0f);
+ case BOTTOM_LEFT, BOTTOM_RIGHT -> matrices.translate(x, ((slot.y + 16f - textRenderer.fontHeight + 2f + 0.7f) * length / 16.0f) - slot.y, 0.0f);
+ }
+ } else {
+ switch (slotText.position()) {
+ case TOP_LEFT -> { /*Do Nothing*/ }
+ case TOP_RIGHT -> matrices.translate(16f - length, 0.0f, 0.0f);
+ case BOTTOM_LEFT -> matrices.translate(0.0f, 16f - textRenderer.fontHeight + 2f, 0.0f);
+ case BOTTOM_RIGHT -> matrices.translate(16f - length, 16f - textRenderer.fontHeight + 2f, 0.0f);
+ }
+ }
+ context.drawText(textRenderer, slotText.text(), slot.x, slot.y, 0xFFFFFF, true);
+ matrices.pop();
+ }
+ }
}
diff --git a/src/main/java/de/hysky/skyblocker/mixins/ItemStackMixin.java b/src/main/java/de/hysky/skyblocker/mixins/ItemStackMixin.java
index 878a93ac..6797cb61 100644
--- a/src/main/java/de/hysky/skyblocker/mixins/ItemStackMixin.java
+++ b/src/main/java/de/hysky/skyblocker/mixins/ItemStackMixin.java
@@ -1,14 +1,22 @@
package de.hysky.skyblocker.mixins;
+import com.google.gson.JsonObject;
import com.llamalad7.mixinextras.injector.ModifyReturnValue;
+import de.hysky.skyblocker.SkyblockerMod;
import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.injected.SkyblockerStack;
+import de.hysky.skyblocker.skyblock.item.tooltip.ItemTooltip;
import de.hysky.skyblocker.utils.ItemUtils;
import de.hysky.skyblocker.utils.Utils;
import it.unimi.dsi.fastutil.ints.IntIntPair;
+import net.minecraft.component.ComponentHolder;
import net.minecraft.component.type.ItemEnchantmentsComponent;
import net.minecraft.item.ItemStack;
import net.minecraft.item.TooltipAppender;
+import net.minecraft.nbt.NbtCompound;
+import net.minecraft.nbt.NbtElement;
import net.minecraft.text.Text;
+import org.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
@@ -17,8 +25,11 @@ import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.ModifyVariable;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+import java.util.Locale;
+import java.util.Optional;
+
@Mixin(ItemStack.class)
-public abstract class ItemStackMixin {
+public abstract class ItemStackMixin implements ComponentHolder, SkyblockerStack {
@Shadow
public abstract int getDamage();
@@ -29,10 +40,19 @@ public abstract class ItemStackMixin {
@Unique
private int maxDamage;
+ @Unique
+ private String skyblockId;
+
+ @Unique
+ private String skyblockApiId;
+
+ @Unique
+ private String neuName;
+
@ModifyReturnValue(method = "getName", at = @At("RETURN"))
private Text skyblocker$customItemNames(Text original) {
if (Utils.isOnSkyblock()) {
- return SkyblockerConfigManager.get().general.customItemNames.getOrDefault(ItemUtils.getItemUuid((ItemStack) (Object) this), original);
+ return SkyblockerConfigManager.get().general.customItemNames.getOrDefault(ItemUtils.getItemUuid(this), original);
}
return original;
@@ -103,4 +123,93 @@ public abstract class ItemStackMixin {
setDamage(durability.rightInt() - durability.leftInt());
return true;
}
+
+ @Override
+ @Nullable
+ public String getSkyblockId() {
+ if (skyblockId != null && !skyblockId.isEmpty()) return skyblockId;
+ return skyblockId = skyblocker$getSkyblockId(true);
+ }
+
+ @Override
+ @Nullable
+ public String getSkyblockApiId() {
+ if (skyblockApiId != null && !skyblockApiId.isEmpty()) return skyblockApiId;
+ return skyblockApiId = skyblocker$getSkyblockId(false);
+ }
+
+ @Override
+ @Nullable
+ public String getNeuName() {
+ if (neuName != null && !neuName.isEmpty()) return neuName;
+ String apiId = getSkyblockApiId();
+ String id = getSkyblockId();
+ if (apiId == null || id == null) return null;
+
+ if (apiId.startsWith("ISSHINY_")) apiId = id;
+
+ return neuName = ItemTooltip.getNeuName(id, apiId);
+ }
+
+ @Unique
+ private String skyblocker$getSkyblockId(boolean internalIDOnly) {
+ NbtCompound customData = ItemUtils.getCustomData((ItemStack) (Object) this);
+
+ if (customData == null || !customData.contains(ItemUtils.ID, NbtElement.STRING_TYPE)) {
+ return null;
+ }
+ String customDataString = customData.getString(ItemUtils.ID);
+
+ if (internalIDOnly) {
+ return customDataString;
+ }
+
+ // Transformation to API format.
+ if (customData.contains("is_shiny")) {
+ return "ISSHINY_" + customDataString;
+ }
+
+ switch (customDataString) {
+ case "ENCHANTED_BOOK" -> {
+ if (customData.contains("enchantments")) {
+ NbtCompound enchants = customData.getCompound("enchantments");
+ Optional<String> firstEnchant = enchants.getKeys().stream().findFirst();
+ String enchant = firstEnchant.orElse("");
+ return "ENCHANTMENT_" + enchant.toUpperCase(Locale.ENGLISH) + "_" + enchants.getInt(enchant);
+ }
+ }
+ case "PET" -> {
+ if (customData.contains("petInfo")) {
+ JsonObject petInfo = SkyblockerMod.GSON.fromJson(customData.getString("petInfo"), JsonObject.class);
+ return "LVL_1_" + petInfo.get("tier").getAsString() + "_" + petInfo.get("type").getAsString();
+ }
+ }
+ case "POTION" -> {
+ String enhanced = customData.contains("enhanced") ? "_ENHANCED" : "";
+ String extended = customData.contains("extended") ? "_EXTENDED" : "";
+ String splash = customData.contains("splash") ? "_SPLASH" : "";
+ if (customData.contains("potion") && customData.contains("potion_level")) {
+ return (customData.getString("potion") + "_" + customDataString + "_" + customData.getInt("potion_level")
+ + enhanced + extended + splash).toUpperCase(Locale.ENGLISH);
+ }
+ }
+ case "RUNE" -> {
+ if (customData.contains("runes")) {
+ NbtCompound runes = customData.getCompound("runes");
+ Optional<String> firstRunes = runes.getKeys().stream().findFirst();
+ String rune = firstRunes.orElse("");
+ return rune.toUpperCase(Locale.ENGLISH) + "_RUNE_" + runes.getInt(rune);
+ }
+ }
+ case "ATTRIBUTE_SHARD" -> {
+ if (customData.contains("attributes")) {
+ NbtCompound shards = customData.getCompound("attributes");
+ Optional<String> firstShards = shards.getKeys().stream().findFirst();
+ String shard = firstShards.orElse("");
+ return customDataString + "-" + shard.toUpperCase(Locale.ENGLISH) + "_" + shards.getInt(shard);
+ }
+ }
+ }
+ return customDataString;
+ }
}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/ChestValue.java b/src/main/java/de/hysky/skyblocker/skyblock/ChestValue.java
index 7f8b2c71..908525e1 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/ChestValue.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/ChestValue.java
@@ -7,13 +7,11 @@ import de.hysky.skyblocker.config.configs.UIAndVisualsConfig;
import de.hysky.skyblocker.mixins.accessors.HandledScreenAccessor;
import de.hysky.skyblocker.mixins.accessors.ScreenAccessor;
import de.hysky.skyblocker.skyblock.item.tooltip.ItemTooltip;
-import de.hysky.skyblocker.skyblock.item.tooltip.TooltipInfoType;
import de.hysky.skyblocker.utils.ItemUtils;
import de.hysky.skyblocker.utils.Utils;
-import it.unimi.dsi.fastutil.longs.LongBooleanPair;
+import it.unimi.dsi.fastutil.doubles.DoubleBooleanPair;
import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents;
import net.fabricmc.fabric.api.client.screen.v1.Screens;
-import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.screen.ingame.GenericContainerScreen;
import net.minecraft.client.gui.tooltip.Tooltip;
import net.minecraft.client.gui.widget.ButtonWidget;
@@ -45,7 +43,7 @@ public class ChestValue {
if (DUNGEON_CHESTS.contains(titleString)) {
if (SkyblockerConfigManager.get().dungeons.dungeonChestProfit.enableProfitCalculator) {
ScreenEvents.afterTick(screen).register(screen_ ->
- ((ScreenAccessor) screen).setTitle(getDungeonChestProfit(genericContainerScreen.getScreenHandler(), title, titleString, client))
+ ((ScreenAccessor) screen).setTitle(getDungeonChestProfit(genericContainerScreen.getScreenHandler(), title, titleString))
);
}
} else if (SkyblockerConfigManager.get().uiAndVisuals.chestValue.enableChestValue && !titleString.equals("SkyBlock Menu")) {
@@ -65,9 +63,9 @@ public class ChestValue {
});
}
- private static Text getDungeonChestProfit(GenericContainerScreenHandler handler, Text title, String titleString, MinecraftClient client) {
+ private static Text getDungeonChestProfit(GenericContainerScreenHandler handler, Text title, String titleString) {
try {
- long profit = 0;
+ double profit = 0;
boolean hasIncompleteData = false, usedKismet = false;
List<Slot> slots = handler.slots.subList(0, handler.getRows() * 9);
@@ -81,16 +79,16 @@ public class ChestValue {
}
String name = stack.getName().getString();
- String id = ItemTooltip.getInternalNameFromNBT(stack, false);
+ String id = stack.getSkyblockApiId();
//Regular item price
if (id != null) {
- LongBooleanPair priceData = getItemPrice(id);
+ DoubleBooleanPair priceData = ItemUtils.getItemPrice(id);
if (!priceData.rightBoolean()) hasIncompleteData = true;
//Add the item price to the profit
- profit += priceData.leftLong() * stack.getCount();
+ profit += priceData.leftDouble() * stack.getCount();
continue;
}
@@ -103,12 +101,12 @@ public class ChestValue {
String type = matcher.group("type");
int amount = Integer.parseInt(matcher.group("amount"));
- LongBooleanPair priceData = getItemPrice(("ESSENCE_" + type).toUpperCase());
+ DoubleBooleanPair priceData = ItemUtils.getItemPrice(("ESSENCE_" + type).toUpperCase());
if (!priceData.rightBoolean()) hasIncompleteData = true;
//Add the price of the essence to the profit
- profit += priceData.leftLong() * amount;
+ profit += priceData.leftDouble() * amount;
continue;
}
@@ -116,7 +114,7 @@ public class ChestValue {
//Determine the cost of the chest
if (name.contains("Open Reward Chest")) {
- String foundString = searchLoreFor(stack, client, "Coins");
+ String foundString = searchLoreFor(stack, "Coins");
//Incase we're searching the free chest
if (!StringUtils.isBlank(foundString)) {
@@ -128,19 +126,19 @@ public class ChestValue {
//Determine if a kismet was used or not
if (name.contains("Reroll Chest")) {
- usedKismet = !StringUtils.isBlank(searchLoreFor(stack, client, "You already rerolled a chest!"));
+ usedKismet = !StringUtils.isBlank(searchLoreFor(stack, "You already rerolled a chest!"));
}
}
if (SkyblockerConfigManager.get().dungeons.dungeonChestProfit.includeKismet && usedKismet) {
- LongBooleanPair kismetPriceData = getItemPrice("KISMET_FEATHER");
+ DoubleBooleanPair kismetPriceData = ItemUtils.getItemPrice("KISMET_FEATHER");
if (!kismetPriceData.rightBoolean()) hasIncompleteData = true;
- profit -= kismetPriceData.leftLong();
+ profit -= kismetPriceData.leftDouble();
}
- return Text.literal(titleString).append(getProfitText(profit, hasIncompleteData));
+ return Text.literal(titleString).append(getProfitText((long) profit, hasIncompleteData));
} catch (Exception e) {
LOGGER.error("[Skyblocker Profit Calculator] Failed to calculate dungeon chest profit! ", e);
}
@@ -150,7 +148,7 @@ public class ChestValue {
private static Text getChestValue(GenericContainerScreenHandler handler, Text title, String titleString) {
try {
- long value = 0;
+ double value = 0;
boolean hasIncompleteData = false;
List<Slot> slots = handler.slots.subList(0, handler.getRows() * 9);
@@ -160,18 +158,18 @@ public class ChestValue {
continue;
}
- String id = ItemTooltip.getInternalNameFromNBT(stack, false);
+ String id = stack.getSkyblockApiId();
if (id != null) {
- LongBooleanPair priceData = getItemPrice(id);
+ DoubleBooleanPair priceData = ItemUtils.getItemPrice(id);
if (!priceData.rightBoolean()) hasIncompleteData = true;
- value += priceData.leftLong() * stack.getCount();
+ value += priceData.leftDouble() * stack.getCount();
}
}
- return Text.literal(titleString).append(getValueText(value, hasIncompleteData));
+ return Text.literal(titleString).append(getValueText((long) value, hasIncompleteData));
} catch (Exception e) {
LOGGER.error("[Skyblocker Value Calculator] Failed to calculate dungeon chest value! ", e);
}
@@ -180,33 +178,9 @@ public class ChestValue {
}
/**
- * @return An {@link LongBooleanPair} with the {@code left long} representing the item's price, and the {@code right boolean} indicating if the price
- * was based on complete data.
- */
- private static LongBooleanPair getItemPrice(String id) {
- JsonObject bazaarPrices = TooltipInfoType.BAZAAR.getData();
- JsonObject lbinPrices = TooltipInfoType.LOWEST_BINS.getData();
-
- if (bazaarPrices == null || lbinPrices == null) return LongBooleanPair.of(0L, false);
-
- if (bazaarPrices.has(id)) {
- JsonObject item = bazaarPrices.get(id).getAsJsonObject();
- boolean isPriceNull = item.get("sellPrice").isJsonNull();
-
- return LongBooleanPair.of(isPriceNull ? 0L : (long) item.get("sellPrice").getAsDouble(), !isPriceNull);
- }
-
- if (lbinPrices.has(id)) {
- return LongBooleanPair.of((long) lbinPrices.get(id).getAsDouble(), true);
- }
-
- return LongBooleanPair.of(0L, false);
- }
-
- /**
* Searches for a specific string of characters in the name and lore of an item
*/
- private static String searchLoreFor(ItemStack stack, MinecraftClient client, String searchString) {
+ private static String searchLoreFor(ItemStack stack, String searchString) {
return ItemUtils.getLoreLineIf(stack, line -> line.contains(searchString));
}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/TeleportOverlay.java b/src/main/java/de/hysky/skyblocker/skyblock/TeleportOverlay.java
index 042b126b..a8155b43 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/TeleportOverlay.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/TeleportOverlay.java
@@ -1,7 +1,6 @@
package de.hysky.skyblocker.skyblock;
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.Utils;
import de.hysky.skyblocker.utils.render.RenderHelper;
@@ -27,7 +26,7 @@ public class TeleportOverlay {
private static void render(WorldRenderContext wrc) {
if (Utils.isOnSkyblock() && SkyblockerConfigManager.get().uiAndVisuals.teleportOverlay.enableTeleportOverlays && client.player != null && client.world != null) {
ItemStack heldItem = client.player.getMainHandStack();
- String itemId = ItemTooltip.getInternalNameFromNBT(heldItem, true);
+ String itemId = heldItem.getSkyblockId();
NbtCompound customData = ItemUtils.getCustomData(heldItem);
if (itemId != null) {
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/auction/AuctionBrowserScreen.java b/src/main/java/de/hysky/skyblocker/skyblock/auction/AuctionBrowserScreen.java
index fd69d886..557cb6c9 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/auction/AuctionBrowserScreen.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/auction/AuctionBrowserScreen.java
@@ -295,8 +295,8 @@ public class AuctionBrowserScreen extends AbstractCustomHypixelGUI<AuctionHouseS
String coins = split[1].replace(",", "").replace("coins", "").trim();
try {
long parsed = Long.parseLong(coins);
- String name = ItemTooltip.getInternalNameFromNBT(stack, false);
- String internalID = ItemTooltip.getInternalNameFromNBT(stack, true);
+ String name = stack.getSkyblockApiId();
+ String internalID = stack.getSkyblockId();
String neuName = name;
if (name == null || internalID == null) break;
if (name.startsWith("ISSHINY_")) {
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/chocolatefactory/ChocolateFactorySolver.java b/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/ChocolateFactorySolver.java
index 2d530b6d..69d2e532 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/ChocolateFactorySolver.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/ChocolateFactorySolver.java
@@ -1,25 +1,24 @@
package de.hysky.skyblocker.skyblock.chocolatefactory;
import de.hysky.skyblocker.config.SkyblockerConfigManager;
-import de.hysky.skyblocker.skyblock.item.tooltip.ItemTooltip;
+import de.hysky.skyblocker.skyblock.item.tooltip.adders.LineSmoothener;
+import de.hysky.skyblocker.skyblock.item.tooltip.TooltipAdder;
import de.hysky.skyblocker.utils.ItemUtils;
import de.hysky.skyblocker.utils.RegexUtils;
+import de.hysky.skyblocker.utils.RomanNumerals;
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.screen.slot.Slot;
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 org.jetbrains.annotations.Nullable;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
@@ -28,6 +27,7 @@ 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");
@@ -36,20 +36,51 @@ public class ChocolateFactorySolver extends ContainerSolver {
private static final Pattern CHOCOLATE_PATTERN = Pattern.compile("^([\\d,]+) Chocolate$");
private static final Pattern PRESTIGE_REQUIREMENT_PATTERN = Pattern.compile("Chocolate this Prestige: ([\\d,]+) +Requires (\\S+) Chocolate this Prestige!");
private static final Pattern TIME_TOWER_STATUS_PATTERN = Pattern.compile("Status: (ACTIVE|INACTIVE)");
- private static final ObjectArrayList<Rabbit> cpsIncreaseFactors = new ObjectArrayList<>(6);
+ private static final Pattern TIME_TOWER_MULTIPLIER_PATTERN = Pattern.compile("by \\+([\\d.]+)x for \\dh\\.");
+
+ 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 isTimeTowerMaxed = false;
private static boolean isTimeTowerActive = false;
+ private static int bestUpgrade = -1;
+ private static int bestAffordableUpgrade = -1;
private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#,###.#", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
- private static ItemStack bestUpgrade = null;
- private static ItemStack bestAffordableUpgrade = null;
+
+ @Override
+ protected void reset() {
+ cpsIncreaseFactors.clear();
+ totalChocolate = -1L;
+ totalCps = -1.0;
+ totalCpsMultiplier = -1.0;
+ requiredUntilNextPrestige = -1L;
+ canPrestige = false;
+ reachedMaxPrestige = false;
+ timeTowerMultiplier = -1.0;
+ isTimeTowerMaxed = false;
+ isTimeTowerActive = false;
+ bestUpgrade = -1;
+ bestAffordableUpgrade = -1;
+ }
+
+ //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);
+ super("^Chocolate Factory$"); //There are multiple screens that fit the pattern `^Chocolate Factory`, so the $ is required
}
@Override
@@ -62,11 +93,12 @@ public class ChocolateFactorySolver extends ContainerSolver {
updateFactoryInfo(slots);
List<ColorHighlight> highlights = new ArrayList<>();
- getPrestigeHighlight(slots.get(28)).ifPresent(highlights::add);
+ 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;
+ bestUpgrade = bestRabbit.slot;
if (bestRabbit.cost <= totalChocolate) {
highlights.add(ColorHighlight.green(bestRabbit.slot));
return highlights;
@@ -75,7 +107,7 @@ public class ChocolateFactorySolver extends ContainerSolver {
for (Rabbit rabbit : cpsIncreaseFactors.subList(1, cpsIncreaseFactors.size())) {
if (rabbit.cost <= totalChocolate) {
- bestAffordableUpgrade = rabbit.itemStack;
+ bestAffordableUpgrade = rabbit.slot;
highlights.add(ColorHighlight.green(rabbit.slot));
break;
}
@@ -87,7 +119,7 @@ public class ChocolateFactorySolver extends ContainerSolver {
private static void updateFactoryInfo(Int2ObjectMap<ItemStack> slots) {
cpsIncreaseFactors.clear();
- for (int i = 29; i <= 33; i++) { // The 5 rabbits slots are in 29, 30, 31, 32 and 33.
+ 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);
@@ -95,20 +127,21 @@ public class ChocolateFactorySolver extends ContainerSolver {
}
//Coach is in slot 42
- getCoach(slots.get(42)).ifPresent(cpsIncreaseFactors::add);
+ 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(13).getName().getString())).ifPresent(l -> totalChocolate = l);
+ 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(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
- Matcher prestigeMatcher = PRESTIGE_REQUIREMENT_PATTERN.matcher(getConcatenatedLore(slots.get(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
@@ -117,12 +150,22 @@ public class ChocolateFactorySolver extends ContainerSolver {
if (NumberUtils.isParsable(amountString)) {
requiredUntilNextPrestige = Long.parseLong(amountString) - currentChocolate.getAsLong();
}
+ canPrestige = false;
+ } 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(39).getName().getString(), ' ')) / 10.0; //The name holds the level, which is multiplier * 10 in roman numerals
- Matcher timeTowerStatusMatcher = TIME_TOWER_STATUS_PATTERN.matcher(getConcatenatedLore(slots.get(39)));
- if (timeTowerStatusMatcher.find()) {
+ isTimeTowerMaxed = StringUtils.substringAfterLast(slots.get(TIME_TOWER_SLOT).getName().getString(), ' ').equals("XV");
+ String timeTowerLore = getConcatenatedLore(slots.get(TIME_TOWER_SLOT));
+ Matcher timeTowerMultiplierMatcher = TIME_TOWER_MULTIPLIER_PATTERN.matcher(timeTowerLore);
+ RegexUtils.getDoubleFromMatcher(timeTowerMultiplierMatcher).ifPresent(d -> timeTowerMultiplier = d);
+ Matcher timeTowerStatusMatcher = TIME_TOWER_STATUS_PATTERN.matcher(timeTowerLore);
+ if (timeTowerStatusMatcher.find(timeTowerMultiplierMatcher.hasMatch() ? timeTowerMultiplierMatcher.end() : 0)) {
isTimeTowerActive = timeTowerStatusMatcher.group(1).equals("ACTIVE");
}
@@ -130,128 +173,6 @@ public class ChocolateFactorySolver extends ContainerSolver {
cpsIncreaseFactors.sort(Comparator.comparingDouble(rabbit -> rabbit.cost() / rabbit.cpsIncrease())); //Ascending order, lower = better
}
- private static void handleTooltip(ItemStack stack, Item.TooltipContext tooltipContext, TooltipType tooltipType, List<Text> lines) {
- if (!SkyblockerConfigManager.get().helpers.chocolateFactory.enableChocolateFactoryHelper) return;
- if (!(MinecraftClient.getInstance().currentScreen instanceof GenericContainerScreen screen) || !screen.getTitle().getString().equals("Chocolate Factory")) return;
-
- int lineIndex = lines.size();
- //This boolean is used to determine if we should add a smooth line to separate the added information from the rest of the tooltip.
- //It should be set to true if there's any information added, false otherwise.
- boolean shouldAddLine = false;
-
- String lore = concatenateLore(lines);
- Matcher costMatcher = COST_PATTERN.matcher(lore);
- OptionalLong cost = RegexUtils.getLongFromMatcher(costMatcher);
- //Available on all items with a chocolate cost
- if (cost.isPresent()) shouldAddLine = addUpgradeTimerToLore(lines, cost.getAsLong());
-
- //Prestige item
- if (stack.isOf(Items.DROPPER) && requiredUntilNextPrestige != -1L) {
- shouldAddLine = addPrestigeTimerToLore(lines) || shouldAddLine;
- }
- //Time tower
- else if (stack.isOf(Items.CLOCK)) {
- shouldAddLine = addTimeTowerStatsToLore(lines) || shouldAddLine;
- }
- //Rabbits
- else if (stack.isOf(Items.PLAYER_HEAD)) {
- shouldAddLine = addRabbitStatsToLore(lines, stack) || shouldAddLine;
- }
-
- //This is an ArrayList, so this operation is probably not very efficient, but logically it's pretty much the only way I can think of
- if (shouldAddLine) lines.add(lineIndex, ItemTooltip.createSmoothLine());
- }
-
- private static boolean addUpgradeTimerToLore(List<Text> lines, long cost) {
- if (totalChocolate < 0L || totalCps < 0.0) return false;
- lines.add(Text.empty()
- .append(Text.literal("Time until upgrade: ").formatted(Formatting.GRAY))
- .append(formatTime((cost - totalChocolate) / totalCps)));
- return true;
- }
-
- private static boolean addPrestigeTimerToLore(List<Text> lines) {
- if (requiredUntilNextPrestige == -1L || totalCps == -1.0) return false;
- lines.add(Text.empty()
- .append(Text.literal("Chocolate until next prestige: ").formatted(Formatting.GRAY))
- .append(Text.literal(DECIMAL_FORMAT.format(requiredUntilNextPrestige)).formatted(Formatting.GOLD)));
- lines.add(Text.empty()
- .append(Text.literal("Time until next prestige: ").formatted(Formatting.GRAY))
- .append(formatTime(requiredUntilNextPrestige / totalCps)));
- return true;
- }
-
- private static boolean addTimeTowerStatsToLore(List<Text> lines) {
- if (totalCps < 0.0 || totalCpsMultiplier < 0.0 || timeTowerMultiplier < 0.0) return false;
- lines.add(Text.literal("Current stats:").formatted(Formatting.GRAY));
- lines.add(Text.empty()
- .append(Text.literal(" CPS increase: ").formatted(Formatting.GRAY))
- .append(Text.literal(DECIMAL_FORMAT.format(totalCps / totalCpsMultiplier * timeTowerMultiplier)).formatted(Formatting.GOLD)));
- lines.add(Text.empty()
- .append(Text.literal(" CPS when active: ").formatted(Formatting.GRAY))
- .append(Text.literal(DECIMAL_FORMAT.format(isTimeTowerActive ? totalCps : totalCps / totalCpsMultiplier * (timeTowerMultiplier + totalCpsMultiplier))).formatted(Formatting.GOLD)));
- if (timeTowerMultiplier < 1.5) {
- lines.add(Text.literal("Stats after upgrade:").formatted(Formatting.GRAY));
- lines.add(Text.empty()
- .append(Text.literal(" CPS increase: ").formatted(Formatting.GRAY))
- .append(Text.literal(DECIMAL_FORMAT.format(totalCps / (totalCpsMultiplier) * (timeTowerMultiplier + 0.1))).formatted(Formatting.GOLD)));
- lines.add(Text.empty()
- .append(Text.literal(" CPS when active: ").formatted(Formatting.GRAY))
- .append(Text.literal(DECIMAL_FORMAT.format(isTimeTowerActive ? totalCps / totalCpsMultiplier * (totalCpsMultiplier + 0.1) : totalCps / totalCpsMultiplier * (timeTowerMultiplier + 0.1 + totalCpsMultiplier))).formatted(Formatting.GOLD)));
- }
- return true;
- }
-
- private static boolean addRabbitStatsToLore(List<Text> lines, ItemStack stack) {
- if (cpsIncreaseFactors.isEmpty()) return false;
- boolean changed = false;
- for (Rabbit rabbit : cpsIncreaseFactors) {
- if (rabbit.itemStack != stack) continue;
- changed = true;
- lines.add(Text.empty()
- .append(Text.literal("CPS Increase: ").formatted(Formatting.GRAY))
- .append(Text.literal(DECIMAL_FORMAT.format(rabbit.cpsIncrease)).formatted(Formatting.GOLD)));
-
- lines.add(Text.empty()
- .append(Text.literal("Cost per CPS: ").formatted(Formatting.GRAY))
- .append(Text.literal(DECIMAL_FORMAT.format(rabbit.cost / rabbit.cpsIncrease)).formatted(Formatting.GOLD)));
-
- if (rabbit.itemStack == bestUpgrade) {
- if (rabbit.cost <= totalChocolate) {
- lines.add(Text.literal("Best upgrade").formatted(Formatting.GREEN));
- } else {
- lines.add(Text.literal("Best upgrade, can't afford").formatted(Formatting.YELLOW));
- }
- } else if (rabbit.itemStack == bestAffordableUpgrade && rabbit.cost <= totalChocolate) {
- lines.add(Text.literal("Best upgrade you can afford").formatted(Formatting.GREEN));
- }
- }
- return changed;
- }
-
- private static MutableText formatTime(double seconds) {
- seconds = Math.ceil(seconds);
- if (seconds <= 0) return Text.literal("Now").formatted(Formatting.GREEN);
-
- StringBuilder builder = new StringBuilder();
- if (seconds >= 86400) {
- builder.append((int) (seconds / 86400)).append("d ");
- seconds %= 86400;
- }
- if (seconds >= 3600) {
- builder.append((int) (seconds / 3600)).append("h ");
- seconds %= 3600;
- }
- if (seconds >= 60) {
- builder.append((int) (seconds / 60)).append("m ");
- seconds %= 60;
- }
- if (seconds >= 1) {
- builder.append((int) seconds).append("s");
- }
- return Text.literal(builder.toString()).formatted(Formatting.GOLD);
- }
-
/**
* Utility method.
*/
@@ -292,7 +213,7 @@ public class ChocolateFactorySolver extends ContainerSolver {
OptionalInt cost = RegexUtils.getIntFromMatcher(costMatcher, multiplierIncreaseMatcher.hasMatch() ? multiplierIncreaseMatcher.end() : 0); //Cost comes after the multiplier line
if (cost.isEmpty()) return Optional.empty();
- return Optional.of(new Rabbit(totalCps / totalCpsMultiplier * (nextCpsMultiplier.getAsDouble() - currentCpsMultiplier.getAsDouble()), cost.getAsInt(), 42, coachItem));
+ return Optional.of(new Rabbit(totalCps / totalCpsMultiplier * (nextCpsMultiplier.getAsDouble() - currentCpsMultiplier.getAsDouble()), cost.getAsInt(), COACH_SLOT));
}
private static Optional<Rabbit> getRabbit(ItemStack item, int slot) {
@@ -309,40 +230,159 @@ public class ChocolateFactorySolver extends ContainerSolver {
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));
+ return Optional.of(new Rabbit(nextCps.getAsInt() - currentCps.getAsInt(), cost.getAsInt(), slot));
}
- private static Optional<ColorHighlight> getPrestigeHighlight(ItemStack item) {
- List<Text> loreList = ItemUtils.getLore(item);
- if (loreList.isEmpty()) return Optional.empty();
-
- String lore = loreList.getLast().getString(); //The last line holds the text we're looking for
- if (lore.equals("Click to prestige!")) return Optional.of(ColorHighlight.green(28));
- return Optional.of(ColorHighlight.red(28));
+ private 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 record Rabbit(double cpsIncrease, int cost, int slot, ItemStack itemStack) {
+ 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;
}
- //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);
+ private record Rabbit(double cpsIncrease, int cost, int slot) { }
+
+ public static final class Tooltip extends TooltipAdder {
+ public Tooltip() {
+ super("^Chocolate Factory$", 0); //The priority doesn't really matter here as this is the only tooltip adder for the Chocolate Factory.
+ }
+
+ @Override
+ public void addToTooltip(@Nullable Slot focusedSlot, ItemStack stack, List<Text> lines) {
+ if (!SkyblockerConfigManager.get().helpers.chocolateFactory.enableChocolateFactoryHelper || focusedSlot == null) 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());
+
+ int index = focusedSlot.id;
+
+ //Prestige item
+ if (index == PRESTIGE_SLOT) {
+ shouldAddLine |= addPrestigeTimerToLore(lines);
+ }
+ //Time tower
+ else if (index == TIME_TOWER_SLOT) {
+ shouldAddLine |= addTimeTowerStatsToLore(lines);
+ }
+ //Rabbits
+ else if (index == COACH_SLOT || (index >= RABBITS_START && index <= RABBITS_END)) {
+ shouldAddLine |= addRabbitStatsToLore(lines, index);
+ }
+
+ //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, LineSmoothener.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 (!isTimeTowerMaxed) {
+ 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, int slot) {
+ if (cpsIncreaseFactors.isEmpty()) return false;
+ for (Rabbit rabbit : cpsIncreaseFactors) {
+ if (rabbit.slot == slot) {
+ 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.slot == 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.slot == bestAffordableUpgrade && rabbit.cost <= totalChocolate) {
+ lines.add(Text.literal("Best upgrade you can afford").formatted(Formatting.GREEN));
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+
+ 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);
}
- 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
index c4fd7d4d..c3a76632 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/EggFinder.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/EggFinder.java
@@ -1,10 +1,17 @@
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;
@@ -13,6 +20,8 @@ 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;
@@ -25,10 +34,9 @@ import java.util.regex.Pattern;
public class EggFinder {
private static final Pattern eggFoundPattern = Pattern.compile("^(?:HOPPITY'S HUNT You found a Chocolate|You have already collected this Chocolate) (Breakfast|Lunch|Dinner)");
- private static final Pattern newEggPattern = Pattern.compile("^HOPPITY'S HUNT A Chocolate (Breakfast|Lunch|Dinner) Egg has appeared!$");
private static final Logger logger = LoggerFactory.getLogger("Skyblocker Egg Finder");
private static final LinkedList<ArmorStandEntity> armorStandQueue = new LinkedList<>();
- private static final Location[] possibleLocations = {Location.CRIMSON_ISLE, Location.CRYSTAL_HOLLOWS, Location.DUNGEON_HUB, Location.DWARVEN_MINES, Location.HUB, Location.THE_END, Location.THE_PARK, Location.GOLD_MINE};
+ private static 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() {
@@ -39,6 +47,17 @@ public class EggFinder {
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) {
@@ -102,7 +121,9 @@ public class EggFinder {
.append("Found a ")
.append(Text.literal("Chocolate " + eggType + " Egg")
.withColor(eggType.color))
- .append(" at " + entity.getBlockPos().up(2).toShortString() + "!"));
+ .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) {
@@ -124,16 +145,6 @@ public class EggFinder {
logger.error("[Skyblocker Egg Finder] Failed to find egg type for egg found message. Tried to match against: " + matcher.group(0), e);
}
}
-
- //There's only one egg of the same type at any given time, so we can set the changed egg to null
- matcher = newEggPattern.matcher(text.getString());
- if (matcher.find()) {
- try {
- EggType.valueOf(matcher.group(1).toUpperCase()).egg.setValue(null);
- } catch (IllegalArgumentException e) {
- logger.error("[Skyblocker Egg Finder] Failed to find egg type for egg spawn message. Tried to match against: " + matcher.group(0), e);
- }
- }
}
record Egg(ArmorStandEntity entity, Waypoint waypoint) { }
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/TimeTowerReminder.java b/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/TimeTowerReminder.java
index 72cbeb2a..6ed11676 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/TimeTowerReminder.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/chocolatefactory/TimeTowerReminder.java
@@ -2,6 +2,7 @@ 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;
@@ -49,7 +50,7 @@ public class TimeTowerReminder {
}
try (FileWriter writer = new FileWriter(tempFile)) {
- writer.write(String.valueOf(System.currentTimeMillis()));
+ 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);
}
@@ -57,8 +58,9 @@ public class TimeTowerReminder {
private static void sendMessage() {
if (MinecraftClient.getInstance().player == null || !Utils.isOnSkyblock()) return;
- MinecraftClient.getInstance().player.sendMessage(Constants.PREFIX.get().append(Text.literal("Your Chocolate Factory's Time Tower has deactivated!").formatted(Formatting.RED)));
-
+ 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;
@@ -81,6 +83,9 @@ public class TimeTowerReminder {
}
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
+ else {
+ Scheduler.INSTANCE.schedule(TimeTowerReminder::sendMessage, 60 * 60 * 20 - (int) ((System.currentTimeMillis() - time) / 50)); // 50 milliseconds is 1 tick
+ scheduled = true;
+ }
}
}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/CroesusProfit.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/CroesusProfit.java
index 70e785cb..0f459c9d 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/CroesusProfit.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/CroesusProfit.java
@@ -35,13 +35,13 @@ public class CroesusProfit extends ContainerSolver {
protected List<ColorHighlight> getColors(String[] groups, Int2ObjectMap<ItemStack> slots) {
List<ColorHighlight> highlights = new ArrayList<>();
ItemStack bestChest = null, secondBestChest = null;
- long bestValue = 0, secondBestValue = 0; // If negative value of chest - it is out of the question
- long dungeonKeyPriceData = getItemPrice("DUNGEON_CHEST_KEY") * 2; // lesser ones don't worth the hassle
+ double bestValue = 0, secondBestValue = 0; // If negative value of chest - it is out of the question
+ double dungeonKeyPriceData = getItemPrice("DUNGEON_CHEST_KEY") * 2; // lesser ones don't worth the hassle
for (Int2ObjectMap.Entry<ItemStack> entry : slots.int2ObjectEntrySet()) {
ItemStack stack = entry.getValue();
if (stack.getName().getString().contains("Chest")) {
- long value = valueChest(stack);
+ double value = valueChest(stack);
if (value > bestValue) {
secondBestChest = bestChest;
secondBestValue = bestValue;
@@ -68,8 +68,8 @@ public class CroesusProfit extends ContainerSolver {
}
- private long valueChest(@NotNull ItemStack chest) {
- long chestValue = 0;
+ private double valueChest(@NotNull ItemStack chest) {
+ double chestValue = 0;
int chestPrice = 0;
List<String> chestItems = new ArrayList<>();
@@ -107,22 +107,8 @@ public class CroesusProfit extends ContainerSolver {
}
- private long getItemPrice(String itemDisplayName) {
- JsonObject bazaarPrices = TooltipInfoType.BAZAAR.getData();
- JsonObject lbinPrices = TooltipInfoType.LOWEST_BINS.getData();
- long itemValue = 0;
- String id = dungeonDropsNameToId.get(itemDisplayName);
-
- if (bazaarPrices == null || lbinPrices == null) return 0;
-
- if (bazaarPrices.has(id)) {
- JsonObject item = bazaarPrices.get(id).getAsJsonObject();
- boolean isPriceNull = item.get("sellPrice").isJsonNull();
- return (isPriceNull ? 0L : item.get("sellPrice").getAsLong());
- } else if (lbinPrices.has(id)) {
- return lbinPrices.get(id).getAsLong();
- }
- return itemValue;
+ private double getItemPrice(String itemDisplayName) {
+ return ItemUtils.getItemPrice(dungeonDropsNameToId.get(itemDisplayName)).leftDouble();
}
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/FarmingHud.java b/src/main/java/de/hysky/skyblocker/skyblock/garden/FarmingHud.java
index 201e6716..b845e07a 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/garden/FarmingHud.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/garden/FarmingHud.java
@@ -16,6 +16,8 @@ import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents;
import net.fabricmc.fabric.api.event.client.player.ClientPlayerBlockBreakEvents;
import net.minecraft.client.MinecraftClient;
import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.NbtCompound;
+import net.minecraft.nbt.NbtElement;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -33,8 +35,9 @@ import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.lit
public class FarmingHud {
private static final Logger LOGGER = LoggerFactory.getLogger(FarmingHud.class);
public static final NumberFormat NUMBER_FORMAT = NumberFormat.getInstance(Locale.US);
- private static final Pattern COUNTER = Pattern.compile("Counter: (?<count>[\\d,]+) .+");
private static final Pattern FARMING_XP = Pattern.compile("§3\\+(?<xp>\\d+.?\\d*) Farming \\((?<percent>[\\d,]+.?\\d*)%\\)");
+ private static final MinecraftClient client = MinecraftClient.getInstance();
+ private static CounterType counterType = CounterType.NONE;
private static final Deque<IntLongPair> counter = new ArrayDeque<>();
private static final LongPriorityQueue blockBreaks = new LongArrayFIFOQueue();
private static final Queue<FloatLongPair> farmingXp = new ArrayDeque<>();
@@ -43,7 +46,7 @@ public class FarmingHud {
public static void init() {
HudRenderEvents.AFTER_MAIN_HUD.register((context, tickDelta) -> {
if (shouldRender()) {
- if (!counter.isEmpty() && counter.peek().rightLong() + 10_000 < System.currentTimeMillis()) {
+ if (!counter.isEmpty() && counter.peek().rightLong() + 5000 < System.currentTimeMillis()) {
counter.poll();
}
if (!blockBreaks.isEmpty() && blockBreaks.firstLong() + 1000 < System.currentTimeMillis()) {
@@ -53,17 +56,9 @@ public class FarmingHud {
farmingXp.poll();
}
- ItemStack stack = MinecraftClient.getInstance().player.getMainHandStack();
- Matcher matcher = ItemUtils.getLoreLineIfMatch(stack, FarmingHud.COUNTER);
- if (matcher != null) {
- try {
- int count = NUMBER_FORMAT.parse(matcher.group("count")).intValue();
- if (counter.isEmpty() || counter.peekLast().leftInt() != count) {
- counter.offer(IntLongPair.of(count, System.currentTimeMillis()));
- }
- } catch (ParseException e) {
- LOGGER.error("[Skyblocker Farming HUD] Failed to parse counter", e);
- }
+ ItemStack stack = client.player.getMainHandStack();
+ if (stack == null || !tryGetCounter(stack, CounterType.CULTIVATING) && !tryGetCounter(stack, CounterType.COUNTER)) {
+ counterType = CounterType.NONE;
}
FarmingHudWidget.INSTANCE.update();
@@ -92,8 +87,26 @@ public class FarmingHud {
.executes(Scheduler.queueOpenScreenCommand(() -> new FarmingHudConfigScreen(null)))))));
}
+ private static boolean tryGetCounter(ItemStack stack, CounterType counterType) {
+ NbtCompound customData = ItemUtils.getCustomData(stack);
+ if (customData == null || !customData.contains(counterType.nbtKey, NbtElement.NUMBER_TYPE)) return false;
+ int count = customData.getInt(counterType.nbtKey);
+ if (FarmingHud.counterType != counterType) {
+ counter.clear();
+ FarmingHud.counterType = counterType;
+ }
+ if (counter.isEmpty() || counter.peekLast().leftInt() != count) {
+ counter.offer(IntLongPair.of(count, System.currentTimeMillis()));
+ }
+ return true;
+ }
+
private static boolean shouldRender() {
- return SkyblockerConfigManager.get().farming.garden.farmingHud.enableHud && Utils.getLocation() == Location.GARDEN;
+ return SkyblockerConfigManager.get().farming.garden.farmingHud.enableHud && client.player != null && Utils.getLocation() == Location.GARDEN;
+ }
+
+ public static String counterText() {
+ return counterType.text;
}
public static int counter() {
@@ -120,4 +133,18 @@ public class FarmingHud {
public static double farmingXpPerHour() {
return farmingXp.stream().mapToDouble(FloatLongPair::leftFloat).sum() * blockBreaks() * 1800; // Hypixel only sends xp updates around every half a second
}
+
+ public enum CounterType {
+ NONE("", "No Counter: "),
+ COUNTER("mined_crops", "Counter: "),
+ CULTIVATING("farmed_cultivating", "Cultivating Counter: ");
+
+ private final String nbtKey;
+ private final String text;
+
+ CounterType(String nbtKey, String text) {
+ this.nbtKey = nbtKey;
+ this.text = text;
+ }
+ }
}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/garden/FarmingHudWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/garden/FarmingHudWidget.java
index f12f6fa8..eb444813 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/garden/FarmingHudWidget.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/garden/FarmingHudWidget.java
@@ -1,11 +1,13 @@
package de.hysky.skyblocker.skyblock.garden;
import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.skyblock.itemlist.ItemRepository;
import de.hysky.skyblocker.skyblock.tabhud.util.Ico;
import de.hysky.skyblocker.skyblock.tabhud.widget.Widget;
import de.hysky.skyblocker.skyblock.tabhud.widget.component.PlainTextComponent;
import de.hysky.skyblocker.skyblock.tabhud.widget.component.ProgressComponent;
import de.hysky.skyblocker.utils.ItemUtils;
+import it.unimi.dsi.fastutil.doubles.DoubleBooleanPair;
import net.minecraft.client.MinecraftClient;
import net.minecraft.entity.Entity;
import net.minecraft.item.ItemStack;
@@ -18,31 +20,31 @@ import java.util.Map;
public class FarmingHudWidget extends Widget {
private static final MutableText TITLE = Text.literal("Farming").formatted(Formatting.YELLOW, Formatting.BOLD);
- public static final Map<String, ItemStack> FARMING_TOOLS = Map.ofEntries(
- Map.entry("THEORETICAL_HOE_WHEAT_1", Ico.WHEAT),
- Map.entry("THEORETICAL_HOE_WHEAT_2", Ico.WHEAT),
- Map.entry("THEORETICAL_HOE_WHEAT_3", Ico.WHEAT),
- Map.entry("THEORETICAL_HOE_CARROT_1", Ico.CARROT),
- Map.entry("THEORETICAL_HOE_CARROT_2", Ico.CARROT),
- Map.entry("THEORETICAL_HOE_CARROT_3", Ico.CARROT),
- Map.entry("THEORETICAL_HOE_POTATO_1", Ico.POTATO),
- Map.entry("THEORETICAL_HOE_POTATO_2", Ico.POTATO),
- Map.entry("THEORETICAL_HOE_POTATO_3", Ico.POTATO),
- Map.entry("THEORETICAL_HOE_CANE_1", Ico.SUGAR_CANE),
- Map.entry("THEORETICAL_HOE_CANE_2", Ico.SUGAR_CANE),
- Map.entry("THEORETICAL_HOE_CANE_3", Ico.SUGAR_CANE),
- Map.entry("THEORETICAL_HOE_WARTS_1", Ico.NETHER_WART),
- Map.entry("THEORETICAL_HOE_WARTS_2", Ico.NETHER_WART),
- Map.entry("THEORETICAL_HOE_WARTS_3", Ico.NETHER_WART),
- Map.entry("FUNGI_CUTTER", Ico.MUSHROOM),
- Map.entry("CACTUS_KNIFE", Ico.CACTUS),
- Map.entry("MELON_DICER", Ico.MELON),
- Map.entry("MELON_DICER_2", Ico.MELON),
- Map.entry("MELON_DICER_3", Ico.MELON),
- Map.entry("PUMPKIN_DICER", Ico.PUMPKIN),
- Map.entry("PUMPKIN_DICER_2", Ico.PUMPKIN),
- Map.entry("PUMPKIN_DICER_3", Ico.PUMPKIN),
- Map.entry("COCO_CHOPPER", Ico.COCOA_BEANS)
+ public static final Map<String, String> FARMING_TOOLS = Map.ofEntries(
+ Map.entry("THEORETICAL_HOE_WHEAT_1", "WHEAT"),
+ Map.entry("THEORETICAL_HOE_WHEAT_2", "WHEAT"),
+ Map.entry("THEORETICAL_HOE_WHEAT_3", "WHEAT"),
+ Map.entry("THEORETICAL_HOE_CARROT_1", "CARROT_ITEM"),
+ Map.entry("THEORETICAL_HOE_CARROT_2", "CARROT_ITEM"),
+ Map.entry("THEORETICAL_HOE_CARROT_3", "CARROT_ITEM"),
+ Map.entry("THEORETICAL_HOE_POTATO_1", "POTATO_ITEM"),
+ Map.entry("THEORETICAL_HOE_POTATO_2", "POTATO_ITEM"),
+ Map.entry("THEORETICAL_HOE_POTATO_3", "POTATO_ITEM"),
+ Map.entry("THEORETICAL_HOE_CANE_1", "SUGAR_CANE"),
+ Map.entry("THEORETICAL_HOE_CANE_2", "SUGAR_CANE"),
+ Map.entry("THEORETICAL_HOE_CANE_3", "SUGAR_CANE"),
+ Map.entry("THEORETICAL_HOE_WARTS_1", "NETHER_STALK"),
+ Map.entry("THEORETICAL_HOE_WARTS_2", "NETHER_STALK"),
+ Map.entry("THEORETICAL_HOE_WARTS_3", "NETHER_STALK"),
+ Map.entry("FUNGI_CUTTER", "RED_MUSHROOM"),
+ Map.entry("CACTUS_KNIFE", "CACTUS"),
+ Map.entry("MELON_DICER", "MELON"),
+ Map.entry("MELON_DICER_2", "MELON"),
+ Map.entry("MELON_DICER_3", "MELON"),
+ Map.entry("PUMPKIN_DICER", "PUMPKIN"),
+ Map.entry("PUMPKIN_DICER_2", "PUMPKIN"),
+ Map.entry("PUMPKIN_DICER_3", "PUMPKIN"),
+ Map.entry("COCO_CHOPPER", "INK_SACK:3")
);
public static final FarmingHudWidget INSTANCE = new FarmingHudWidget();
private final MinecraftClient client = MinecraftClient.getInstance();
@@ -56,19 +58,28 @@ public class FarmingHudWidget extends Widget {
@Override
public void updateContent() {
- ItemStack icon = client.player == null ? Ico.HOE : FARMING_TOOLS.getOrDefault(ItemUtils.getItemId(client.player.getMainHandStack()), Ico.HOE);
- addSimpleIcoText(icon, "Counter: ", Formatting.YELLOW, FarmingHud.NUMBER_FORMAT.format(FarmingHud.counter()));
- addSimpleIcoText(icon, "Crops/min: ", Formatting.YELLOW, FarmingHud.NUMBER_FORMAT.format((int) FarmingHud.cropsPerMinute() / 100 * 100));
- addSimpleIcoText(icon, "Blocks/s: ", Formatting.YELLOW, Integer.toString(FarmingHud.blockBreaks()));
+ if (client.player == null) return;
+ ItemStack farmingToolStack = client.player.getMainHandStack();
+ if (farmingToolStack == null) return;
+ String cropItemId = FARMING_TOOLS.get(ItemUtils.getItemId(farmingToolStack));
+ ItemStack cropStack = ItemRepository.getItemStack(cropItemId);
+
+ addSimpleIcoText(cropStack, FarmingHud.counterText(), Formatting.YELLOW, FarmingHud.NUMBER_FORMAT.format(FarmingHud.counter()));
+ float cropsPerMinute = FarmingHud.cropsPerMinute();
+ addSimpleIcoText(cropStack, "Crops/min: ", Formatting.YELLOW, FarmingHud.NUMBER_FORMAT.format((int) cropsPerMinute / 10 * 10));
+ DoubleBooleanPair itemPrice = ItemUtils.getItemPrice(cropItemId);
+ addSimpleIcoText(Ico.GOLD, "Coins/h: ", Formatting.GOLD, itemPrice.rightBoolean() ? FarmingHud.NUMBER_FORMAT.format((int) (itemPrice.leftDouble() * cropsPerMinute * 0.6) * 100) : "No Data"); // Multiply by 60 to convert to hourly and divide by 100 for rounding is combined into multiplying by 0.6
+
+ addSimpleIcoText(cropStack, "Blocks/s: ", Formatting.YELLOW, Integer.toString(FarmingHud.blockBreaks()));
//noinspection DataFlowIssue
addComponent(new ProgressComponent(Ico.LANTERN, Text.literal("Farming Level: "), FarmingHud.farmingXpPercentProgress(), Formatting.GOLD.getColorValue()));
addSimpleIcoText(Ico.LIME_DYE, "Farming XP/h: ", Formatting.YELLOW, FarmingHud.NUMBER_FORMAT.format((int) FarmingHud.farmingXpPerHour()));
Entity cameraEntity = client.getCameraEntity();
- double yaw = cameraEntity == null ? 0.0d : cameraEntity.getYaw();
- double pitch = cameraEntity == null ? 0.0d : cameraEntity.getPitch();
- addComponent(new PlainTextComponent(Text.literal("Yaw: " + String.format("%.3f", MathHelper.wrapDegrees(yaw))).formatted(Formatting.YELLOW)));
- addComponent(new PlainTextComponent(Text.literal("Pitch: " + String.format("%.3f", MathHelper.wrapDegrees(pitch))).formatted(Formatting.YELLOW)));
+ String yaw = cameraEntity == null ? "No Camera Entity" : String.format("%.2f", MathHelper.wrapDegrees(cameraEntity.getYaw()));
+ String pitch = cameraEntity == null ? "No Camera Entity" : String.format("%.2f", MathHelper.wrapDegrees(cameraEntity.getPitch()));
+ addComponent(new PlainTextComponent(Text.literal("Yaw: " + yaw).formatted(Formatting.GOLD)));
+ addComponent(new PlainTextComponent(Text.literal("Pitch: " + pitch).formatted(Formatting.GOLD)));
if (LowerSensitivity.isSensitivityLowered()) {
addComponent(new PlainTextComponent(Text.translatable("skyblocker.garden.hud.mouseLocked").formatted(Formatting.ITALIC)));
}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/garden/LowerSensitivity.java b/src/main/java/de/hysky/skyblocker/skyblock/garden/LowerSensitivity.java
index ee857618..2bfb7fb2 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/garden/LowerSensitivity.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/garden/LowerSensitivity.java
@@ -9,28 +9,23 @@ import net.minecraft.client.MinecraftClient;
import net.minecraft.item.ItemStack;
public class LowerSensitivity {
-
private static boolean sensitivityLowered = false;
public static void init() {
ClientTickEvents.END_WORLD_TICK.register(world -> {
- if (!Utils.isOnSkyblock() || Utils.getLocation() != Location.GARDEN || MinecraftClient.getInstance().player == null) {
+ if (Utils.getLocation() != Location.GARDEN || MinecraftClient.getInstance().player == null || !SkyblockerConfigManager.get().farming.garden.lockMouseTool) {
if (sensitivityLowered) lowerSensitivity(false);
return;
}
- if (SkyblockerConfigManager.get().farming.garden.lockMouseTool) {
- ItemStack mainHandStack = MinecraftClient.getInstance().player.getMainHandStack();
- String itemId = ItemUtils.getItemId(mainHandStack);
- boolean shouldLockMouse = FarmingHudWidget.FARMING_TOOLS.containsKey(itemId) && (!SkyblockerConfigManager.get().farming.garden.lockMouseGroundOnly || MinecraftClient.getInstance().player.isOnGround());
- if (shouldLockMouse && !sensitivityLowered) lowerSensitivity(true);
- else if (!shouldLockMouse && sensitivityLowered) lowerSensitivity(false);
-
- }
+ ItemStack mainHandStack = MinecraftClient.getInstance().player.getMainHandStack();
+ String itemId = ItemUtils.getItemId(mainHandStack);
+ boolean shouldLockMouse = FarmingHudWidget.FARMING_TOOLS.containsKey(itemId) && (!SkyblockerConfigManager.get().farming.garden.lockMouseGroundOnly || MinecraftClient.getInstance().player.isOnGround());
+ if (shouldLockMouse && !sensitivityLowered) lowerSensitivity(true);
+ else if (!shouldLockMouse && sensitivityLowered) lowerSensitivity(false);
});
}
public static void lowerSensitivity(boolean lowerSensitivity) {
- if (sensitivityLowered == lowerSensitivity) return;
sensitivityLowered = lowerSensitivity;
}
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/slottext/SlotText.java b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/SlotText.java
new file mode 100644
index 00000000..66c02ca1
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/SlotText.java
@@ -0,0 +1,21 @@
+package de.hysky.skyblocker.skyblock.item.slottext;
+
+import net.minecraft.text.Text;
+
+public record SlotText(Text text, TextPosition position) {
+ public static SlotText bottomLeft(Text text) {
+ return new SlotText(text, TextPosition.BOTTOM_LEFT);
+ }
+
+ public static SlotText bottomRight(Text text) {
+ return new SlotText(text, TextPosition.BOTTOM_RIGHT);
+ }
+
+ public static SlotText topLeft(Text text) {
+ return new SlotText(text, TextPosition.TOP_LEFT);
+ }
+
+ public static SlotText topRight(Text text) {
+ return new SlotText(text, TextPosition.TOP_RIGHT);
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/SlotTextAdder.java b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/SlotTextAdder.java
new file mode 100644
index 00000000..71d9aa30
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/SlotTextAdder.java
@@ -0,0 +1,58 @@
+package de.hysky.skyblocker.skyblock.item.slottext;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.render.gui.AbstractContainerMatcher;
+import net.minecraft.screen.slot.Slot;
+import org.intellij.lang.annotations.Language;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+import java.util.regex.Pattern;
+
+/**
+ * Extend this class and add it to {@link SlotTextManager#adders} to add text to any arbitrary slot.
+ */
+public abstract class SlotTextAdder extends AbstractContainerMatcher {
+ /**
+ * Utility constructor that will compile the given string into a pattern.
+ *
+ * @see #SlotTextAdder(Pattern)
+ */
+ protected SlotTextAdder(@NotNull @Language("RegExp") String titlePattern) {
+ super(titlePattern);
+ }
+
+ /**
+ * Creates a SlotTextAdder that will be applied to screens with titles that match the given pattern.
+ *
+ * @param titlePattern The pattern to match the screen title against.
+ */
+ protected SlotTextAdder(@NotNull Pattern titlePattern) {
+ super(titlePattern);
+ }
+
+ /**
+ * Creates a SlotTextAdder that will be applied to all screens.
+ */
+ protected SlotTextAdder() {
+ super();
+ }
+
+ /**
+ * This method will be called for each rendered slot. Consider using a switch statement on {@link Slot#id} if you wish to add different text to different slots.
+ *
+ * @return A list of positioned text to be rendered. Return {@link List#of()} if no text should be rendered.
+ * @implNote By minecraft's design, scaled text inexplicably moves around.
+ * So, limit your text to 3 characters (or roughly less than 20 width) if you want it to not look horrible.
+ */
+ public abstract @NotNull List<SlotText> getText(Slot slot);
+
+ /**
+ * Override this method to add conditions to enable or disable this adder.
+ * @return Whether this adder is enabled.
+ * @implNote The slot text adders only work while in skyblock, so no need to check for that again.
+ */
+ public boolean isEnabled() {
+ return SkyblockerConfigManager.get().general.itemInfoDisplay.slotText;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/SlotTextManager.java b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/SlotTextManager.java
new file mode 100644
index 00000000..18131527
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/SlotTextManager.java
@@ -0,0 +1,71 @@
+package de.hysky.skyblocker.skyblock.item.slottext;
+
+import de.hysky.skyblocker.skyblock.item.slottext.adders.*;
+import de.hysky.skyblocker.utils.Utils;
+import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents;
+import net.minecraft.client.gui.screen.Screen;
+import net.minecraft.client.gui.screen.ingame.HandledScreen;
+import net.minecraft.screen.slot.Slot;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class SlotTextManager {
+ private static final SlotTextAdder[] adders = new SlotTextAdder[]{
+ new EnchantmentLevelAdder(),
+ new MinionLevelAdder(),
+ new PetLevelAdder(),
+ new SkyblockLevelAdder(),
+ new SkillLevelAdder(),
+ new CatacombsLevelAdder.Dungeoneering(),
+ new CatacombsLevelAdder.DungeonClasses(),
+ new CatacombsLevelAdder.ReadyUp(),
+ new RancherBootsSpeedAdder(),
+ new AttributeShardAdder(),
+ new PrehistoricEggAdder(),
+ new PotionLevelAdder(),
+ new CollectionAdder(),
+ new CommunityShopAdder()
+ };
+ private static final ArrayList<SlotTextAdder> currentScreenAdders = new ArrayList<>();
+
+ private SlotTextManager() {
+ }
+
+ public static void init() {
+ ScreenEvents.AFTER_INIT.register((client, screen, width, height) -> {
+ if (screen instanceof HandledScreen<?> && Utils.isOnSkyblock()) {
+ onScreenChange(screen);
+ ScreenEvents.remove(screen).register(ignored -> currentScreenAdders.clear());
+ }
+ });
+ }
+
+ private static void onScreenChange(Screen screen) {
+ final String title = screen.getTitle().getString();
+ for (SlotTextAdder adder : adders) {
+ if (!adder.isEnabled()) continue;
+ if (adder.titlePattern == null || adder.titlePattern.matcher(title).find()) {
+ currentScreenAdders.add(adder);
+ }
+ }
+ }
+
+ /**
+ * The returned text is rendered on top of the slot. The text will be scaled if it doesn't fit in the slot,
+ * but 3 characters should be seen as the maximum to keep it readable and in place as it tends to move around when scaled.
+ *
+ * @implNote Only the first adder that returns a non-null text will be used.
+ * The order of the adders remains the same as they were added to the {@link SlotTextManager#adders} array.
+ */
+ @NotNull
+ public static List<SlotText> getText(Slot slot) {
+ if (currentScreenAdders.isEmpty()) return List.of();
+ for (SlotTextAdder adder : currentScreenAdders) {
+ List<SlotText> text = adder.getText(slot);
+ if (!text.isEmpty()) return text;
+ }
+ return List.of();
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/TextPosition.java b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/TextPosition.java
new file mode 100644
index 00000000..052b228d
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/TextPosition.java
@@ -0,0 +1,8 @@
+package de.hysky.skyblocker.skyblock.item.slottext;
+
+public enum TextPosition {
+ TOP_LEFT,
+ TOP_RIGHT,
+ BOTTOM_LEFT,
+ BOTTOM_RIGHT
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/AttributeShards.java b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/AttributeShardAdder.java
index ed650e26..811677d7 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/item/AttributeShards.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/AttributeShardAdder.java
@@ -1,9 +1,22 @@
-package de.hysky.skyblocker.skyblock.item;
+package de.hysky.skyblocker.skyblock.item.slottext.adders;
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.skyblock.item.slottext.SlotText;
+import de.hysky.skyblocker.skyblock.item.slottext.SlotTextAdder;
+import de.hysky.skyblocker.utils.ItemUtils;
+import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.NbtCompound;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import org.jetbrains.annotations.NotNull;
-public class AttributeShards {
- private static final Object2ObjectOpenHashMap<String, String> ID_2_SHORT_NAME = new Object2ObjectOpenHashMap<>();
+import java.util.List;
+
+public class AttributeShardAdder extends SlotTextAdder {
+ private static final Object2ObjectMap<String, String> ID_2_SHORT_NAME = new Object2ObjectOpenHashMap<>();
static {
//Weapons
@@ -50,10 +63,35 @@ public class AttributeShards {
ID_2_SHORT_NAME.put("fishing_speed", "FS");
ID_2_SHORT_NAME.put("hunter", "H");
ID_2_SHORT_NAME.put("trophy_hunter", "TH");
+ }
+
+ public AttributeShardAdder() {
+ super();
+ }
+
+ @Override
+ public @NotNull List<SlotText> getText(Slot slot) {
+ final ItemStack stack = slot.getStack();
+ NbtCompound customData = ItemUtils.getCustomData(stack);
+
+ if (!ItemUtils.getItemId(stack).equals("ATTRIBUTE_SHARD")) return List.of();
+
+ NbtCompound attributesTag = customData.getCompound("attributes");
+ String[] attributes = attributesTag.getKeys().toArray(String[]::new);
+
+ if (attributes.length != 1) return List.of();
+ String attributeId = attributes[0];
+ int attributeLevel = attributesTag.getInt(attributeId);
+ String attributeInitials = ID_2_SHORT_NAME.getOrDefault(attributeId, "");
+ return List.of(
+ SlotText.bottomRight(Text.literal(String.valueOf(attributeLevel)).withColor(0x34eb77)),
+ SlotText.topLeft(Text.literal(attributeInitials).formatted(Formatting.AQUA))
+ );
}
- public static String getShortName(String id) {
- return ID_2_SHORT_NAME.getOrDefault(id, "");
+ @Override
+ public boolean isEnabled() {
+ return SkyblockerConfigManager.get().general.itemInfoDisplay.attributeShardInfo;
}
}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/CatacombsLevelAdder.java b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/CatacombsLevelAdder.java
new file mode 100644
index 00000000..da12e867
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/CatacombsLevelAdder.java
@@ -0,0 +1,105 @@
+package de.hysky.skyblocker.skyblock.item.slottext.adders;
+
+import de.hysky.skyblocker.skyblock.item.slottext.SlotText;
+import de.hysky.skyblocker.skyblock.item.slottext.SlotTextAdder;
+import de.hysky.skyblocker.utils.RomanNumerals;
+import net.minecraft.item.ItemStack;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import org.apache.commons.lang3.math.NumberUtils;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+//This class is split into 3 inner classes as there are multiple screens for showing catacombs levels, each with different slot ids or different style of showing the level.
+//It's still kept in 1 main class for organization purposes.
+public class CatacombsLevelAdder {
+ private CatacombsLevelAdder() {
+ }
+
+ public static class Dungeoneering extends SlotTextAdder {
+ private static final Pattern LEVEL_PATTERN = Pattern.compile(".*?(?:(?<arabic>\\d+)|(?<roman>\\S+))? ?✯?");
+ public Dungeoneering() {
+ super("^Dungeoneering");
+ }
+
+ @Override
+ public @NotNull List<SlotText> getText(Slot slot) {
+ switch (slot.id) {
+ case 12, 29, 30, 31, 32, 33 -> {
+ Matcher matcher = LEVEL_PATTERN.matcher(slot.getStack().getName().getString());
+ if (!matcher.matches()) return List.of();
+ String arabic = matcher.group("arabic");
+ String roman = matcher.group("roman");
+ if (arabic == null && roman == null) return List.of(SlotText.bottomLeft(Text.literal("0").formatted(Formatting.RED)));
+ String level;
+ if (arabic != null) {
+ if (!NumberUtils.isDigits(arabic)) return List.of(); //Sanity check
+ level = arabic;
+ } else { // roman != null
+ if (!RomanNumerals.isValidRomanNumeral(roman)) return List.of(); //Sanity check
+ level = String.valueOf(RomanNumerals.romanToDecimal(roman));
+ }
+
+ return List.of(SlotText.bottomLeft(Text.literal(level).formatted(Formatting.RED)));
+ }
+ default -> {
+ return List.of();
+ }
+ }
+ }
+ }
+
+ public static class DungeonClasses extends SlotTextAdder {
+
+ public DungeonClasses() {
+ super("^Dungeon Classes"); //Applies to both screens as they are same in both the placement and the style of the level text.
+ }
+
+ @Override
+ public @NotNull List<SlotText> getText(Slot slot) {
+ switch (slot.id) {
+ case 11, 12, 13, 14, 15 -> {
+ String level = getBracketedLevelFromName(slot.getStack());
+ if (!NumberUtils.isDigits(level)) return List.of();
+ return List.of(SlotText.bottomLeft(Text.literal(level).formatted(Formatting.RED)));
+ }
+ default -> {
+ return List.of();
+ }
+ }
+ }
+ }
+
+ public static class ReadyUp extends SlotTextAdder {
+
+ public ReadyUp() {
+ super("^Ready Up");
+ }
+
+ @Override
+ public @NotNull List<SlotText> getText(Slot slot) {
+ switch (slot.id) {
+ case 29, 30, 31, 32, 33 -> {
+ String level = getBracketedLevelFromName(slot.getStack());
+ if (!NumberUtils.isDigits(level)) return List.of();
+ return List.of(SlotText.bottomLeft(Text.literal(level).formatted(Formatting.RED)));
+ }
+ default -> {
+ return List.of();
+ }
+ }
+ }
+ }
+
+ public static String getBracketedLevelFromName(ItemStack itemStack) {
+ String name = itemStack.getName().getString();
+ if (!name.startsWith("[Lvl ")) return null;
+ int index = name.indexOf(']');
+ if (index == -1) return null;
+ return name.substring(5, index);
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/CollectionAdder.java b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/CollectionAdder.java
new file mode 100644
index 00000000..e577f0d8
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/CollectionAdder.java
@@ -0,0 +1,33 @@
+package de.hysky.skyblocker.skyblock.item.slottext.adders;
+
+import de.hysky.skyblocker.skyblock.item.slottext.SlotText;
+import de.hysky.skyblocker.skyblock.item.slottext.SlotTextAdder;
+import de.hysky.skyblocker.utils.RomanNumerals;
+import net.minecraft.item.ItemStack;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class CollectionAdder extends SlotTextAdder {
+ private static final Pattern COLLECTION = Pattern.compile("^[\\w ]+ (?<level>[IVXLCDM]+)$");
+
+ public CollectionAdder() {
+ super("^\\w+ Collections");
+ }
+
+ @Override
+ public @NotNull List<SlotText> getText(Slot slot) {
+ final ItemStack stack = slot.getStack();
+ Matcher matcher = COLLECTION.matcher(stack.getName().getString());
+ if (matcher.matches()) {
+ int level = RomanNumerals.romanToDecimal(matcher.group("level"));
+ return List.of(SlotText.bottomRight(Text.literal(String.valueOf(level)).formatted(Formatting.YELLOW)));
+ }
+ return List.of();
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/CommunityShopAdder.java b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/CommunityShopAdder.java
new file mode 100644
index 00000000..c7ea17dc
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/CommunityShopAdder.java
@@ -0,0 +1,60 @@
+package de.hysky.skyblocker.skyblock.item.slottext.adders;
+
+import de.hysky.skyblocker.skyblock.item.slottext.SlotText;
+import de.hysky.skyblocker.skyblock.item.slottext.SlotTextAdder;
+import de.hysky.skyblocker.utils.ItemUtils;
+import de.hysky.skyblocker.utils.RomanNumerals;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.Items;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+public class CommunityShopAdder extends SlotTextAdder {
+ private static final byte CATEGORIES_START = 10;
+ private static final byte CATEGORIES_END = 14; //Inclusive
+
+ public CommunityShopAdder() {
+ super("^Community Shop");
+ }
+
+ @Override
+ public @NotNull List<SlotText> getText(Slot slot) {
+ for (byte i = CATEGORIES_START; i <= CATEGORIES_END; i++) {
+ if (slot.inventory.getStack(i).isOf(Items.LIME_STAINED_GLASS_PANE)) { //Only the selected category has a lime stained glass pane, the others have a gray one.
+ return switch (i) { //This is a switch to allow adding more categories easily in the future, if we ever add more.
+ case 11 -> getTextForUpgradesScreen(slot);
+ default -> List.of();
+ };
+ }
+ }
+ return List.of();
+ }
+
+ private static List<SlotText> getTextForUpgradesScreen(Slot slot) {
+ final ItemStack stack = slot.getStack();
+ switch (slot.id) {
+ case 30, 31, 32, 33, 34, 38, 39, 40, 41, 42, 43, 44 -> {
+ String name = stack.getName().getString();
+ int lastIndex = name.lastIndexOf(' ');
+ String roman = name.substring(lastIndex + 1); // + 1 as we don't want the space
+ if (!RomanNumerals.isValidRomanNumeral(roman)) return List.of();
+
+ List<Text> lore = ItemUtils.getLore(stack);
+ if (lore.isEmpty()) return List.of();
+ String lastLine = lore.getLast().getString();
+ return List.of(SlotText.bottomLeft(switch (lastLine) {
+ case "Maxed out!" -> Text.literal("Max").withColor(0xfab387);
+ case "Currently upgrading!" -> Text.literal("⏰").withColor(0xf9e2af).formatted(Formatting.BOLD);
+ case "Click to claim!" -> Text.literal("✅").withColor(0xa6e3a1).formatted(Formatting.BOLD);
+ default -> Text.literal(String.valueOf(RomanNumerals.romanToDecimal(roman))).withColor(0xcba6f7);
+ }));
+
+ }
+ }
+ return List.of();
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/EnchantmentLevelAdder.java b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/EnchantmentLevelAdder.java
new file mode 100644
index 00000000..9c85ae61
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/EnchantmentLevelAdder.java
@@ -0,0 +1,46 @@
+package de.hysky.skyblocker.skyblock.item.slottext.adders;
+
+import de.hysky.skyblocker.skyblock.item.slottext.SlotText;
+import de.hysky.skyblocker.skyblock.item.slottext.SlotTextAdder;
+import de.hysky.skyblocker.utils.ItemUtils;
+import de.hysky.skyblocker.utils.RomanNumerals;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.Items;
+import net.minecraft.nbt.NbtCompound;
+import net.minecraft.nbt.NbtElement;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+public class EnchantmentLevelAdder extends SlotTextAdder {
+ public EnchantmentLevelAdder() {
+ super();
+ }
+
+ @Override
+ public @NotNull List<SlotText> getText(Slot slot) {
+ final ItemStack itemStack = slot.getStack();
+ if (!itemStack.isOf(Items.ENCHANTED_BOOK)) return List.of();
+ String name = itemStack.getName().getString();
+ if (name.equals("Enchanted Book")) {
+ NbtCompound nbt = ItemUtils.getCustomData(itemStack);
+ if (nbt.isEmpty() || !nbt.contains("enchantments", NbtElement.COMPOUND_TYPE)) return List.of();
+ NbtCompound enchantments = nbt.getCompound("enchantments");
+ if (enchantments.getSize() != 1) return List.of(); //Only makes sense to display the level when there's one enchant.
+ int level = enchantments.getInt(enchantments.getKeys().iterator().next());
+ return List.of(SlotText.bottomLeft(Text.literal(String.valueOf(level)).formatted(Formatting.GREEN)));
+ } else { //In bazaar, the books have the enchantment level in the name
+ int level = getEnchantLevelFromString(name);
+ if (level == 0) return List.of();
+ return List.of(SlotText.bottomLeft(Text.literal(String.valueOf(level)).formatted(Formatting.GREEN)));
+ }
+ }
+
+ private static int getEnchantLevelFromString(String str) {
+ String romanNumeral = str.substring(str.lastIndexOf(' ') + 1); //+1 because we don't need the space itself
+ return RomanNumerals.romanToDecimal(romanNumeral); //Temporary line. The method will be moved out later.
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/MinionLevelAdder.java b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/MinionLevelAdder.java
new file mode 100644
index 00000000..b9fe130f
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/MinionLevelAdder.java
@@ -0,0 +1,34 @@
+package de.hysky.skyblocker.skyblock.item.slottext.adders;
+
+import de.hysky.skyblocker.skyblock.item.slottext.SlotText;
+import de.hysky.skyblocker.skyblock.item.slottext.SlotTextAdder;
+import de.hysky.skyblocker.utils.RomanNumerals;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.Items;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class MinionLevelAdder extends SlotTextAdder {
+ private static final Pattern MINION_PATTERN = Pattern.compile(".* Minion ([IVXLCDM]+)");
+ public MinionLevelAdder() {
+ super();
+ }
+
+ @Override
+ public @NotNull List<SlotText> getText(Slot slot) {
+ ItemStack itemStack = slot.getStack();
+ if (!itemStack.isOf(Items.PLAYER_HEAD)) return List.of();
+ Matcher matcher = MINION_PATTERN.matcher(itemStack.getName().getString());
+ if (!matcher.matches()) return List.of();
+ String romanNumeral = matcher.group(1);
+ if (!RomanNumerals.isValidRomanNumeral(romanNumeral)) return List.of();
+ int level = RomanNumerals.romanToDecimal(romanNumeral);
+ return List.of(SlotText.topRight(Text.literal(String.valueOf(level)).formatted(Formatting.AQUA)));
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/PetLevelAdder.java b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/PetLevelAdder.java
new file mode 100644
index 00000000..3813563a
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/PetLevelAdder.java
@@ -0,0 +1,28 @@
+package de.hysky.skyblocker.skyblock.item.slottext.adders;
+
+import de.hysky.skyblocker.skyblock.item.slottext.SlotText;
+import de.hysky.skyblocker.skyblock.item.slottext.SlotTextAdder;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.Items;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import org.apache.commons.lang3.math.NumberUtils;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+public class PetLevelAdder extends SlotTextAdder {
+ public PetLevelAdder() {
+ super();
+ }
+
+ @Override
+ public @NotNull List<SlotText> getText(Slot slot) {
+ ItemStack itemStack = slot.getStack();
+ if (!itemStack.isOf(Items.PLAYER_HEAD)) return List.of();
+ String level = CatacombsLevelAdder.getBracketedLevelFromName(itemStack);
+ if (!NumberUtils.isDigits(level)) return List.of();
+ return List.of(SlotText.topLeft(Text.literal(level).formatted(Formatting.GOLD)));
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/PotionLevelAdder.java b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/PotionLevelAdder.java
new file mode 100644
index 00000000..457d2964
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/PotionLevelAdder.java
@@ -0,0 +1,27 @@
+package de.hysky.skyblocker.skyblock.item.slottext.adders;
+
+import de.hysky.skyblocker.skyblock.item.slottext.SlotText;
+import de.hysky.skyblocker.skyblock.item.slottext.SlotTextAdder;
+import de.hysky.skyblocker.utils.ItemUtils;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.NbtCompound;
+import net.minecraft.nbt.NbtElement;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+public class PotionLevelAdder extends SlotTextAdder {
+ @Override
+ public @NotNull List<SlotText> getText(Slot slot) {
+ final ItemStack stack = slot.getStack();
+ NbtCompound customData = ItemUtils.getCustomData(stack);
+ if (customData.contains("potion_level", NbtElement.INT_TYPE)) {
+ int level = customData.getInt("potion_level");
+ return List.of(SlotText.bottomRight(Text.literal(String.valueOf(level)).formatted(Formatting.AQUA)));
+ }
+ return List.of();
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/PrehistoricEggAdder.java b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/PrehistoricEggAdder.java
new file mode 100644
index 00000000..a157efee
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/PrehistoricEggAdder.java
@@ -0,0 +1,34 @@
+package de.hysky.skyblocker.skyblock.item.slottext.adders;
+
+import de.hysky.skyblocker.skyblock.item.slottext.SlotText;
+import de.hysky.skyblocker.skyblock.item.slottext.SlotTextAdder;
+import de.hysky.skyblocker.utils.ItemUtils;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.Items;
+import net.minecraft.nbt.NbtCompound;
+import net.minecraft.nbt.NbtElement;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import org.apache.commons.lang3.StringUtils;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+public class PrehistoricEggAdder extends SlotTextAdder {
+ @Override
+ public @NotNull List<SlotText> getText(Slot slot) {
+ final ItemStack stack = slot.getStack();
+ if (!stack.isOf(Items.PLAYER_HEAD) || !StringUtils.equals(stack.getSkyblockId(), "PREHISTORIC_EGG")) return List.of();
+ NbtCompound nbt = ItemUtils.getCustomData(stack);
+ if (!nbt.contains("blocks_walked", NbtElement.INT_TYPE)) return List.of();
+ int walked = nbt.getInt("blocks_walked");
+
+ String walkedstr;
+ if (walked < 1000) walkedstr = String.valueOf(walked);
+ else if (walked < 10000) walkedstr = String.format("%.1fk", walked/1000.0f);
+ else walkedstr = walked / 1000 + "k";
+
+ return List.of(SlotText.bottomLeft(Text.literal(walkedstr).formatted(Formatting.GOLD)));
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/RancherBootsSpeedAdder.java b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/RancherBootsSpeedAdder.java
new file mode 100644
index 00000000..1f92fb8a
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/RancherBootsSpeedAdder.java
@@ -0,0 +1,36 @@
+package de.hysky.skyblocker.skyblock.item.slottext.adders;
+
+import de.hysky.skyblocker.skyblock.item.slottext.SlotText;
+import de.hysky.skyblocker.skyblock.item.slottext.SlotTextAdder;
+import de.hysky.skyblocker.utils.ItemUtils;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.Items;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import org.apache.commons.lang3.StringUtils;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class RancherBootsSpeedAdder extends SlotTextAdder {
+ private static final Pattern SPEED_PATTERN = Pattern.compile("Current Speed Cap: (\\d+) ?(\\d+)?");
+
+ public RancherBootsSpeedAdder() {
+ super();
+ }
+
+ @Override
+ public @NotNull List<SlotText> getText(Slot slot) {
+ final ItemStack itemStack = slot.getStack();
+ // V null-safe equals.
+ if (!itemStack.isOf(Items.LEATHER_BOOTS) && !StringUtils.equals(itemStack.getSkyblockId(), "RANCHERS_BOOTS")) return List.of();
+ Matcher matcher = ItemUtils.getLoreLineIfMatch(slot.getStack(), SPEED_PATTERN);
+ if (matcher == null) return List.of();
+ String speed = matcher.group(2);
+ if (speed == null) speed = matcher.group(1); //2nd group only matches when the speed cap is set to a number beyond the player's actual speed cap.
+ return List.of(SlotText.bottomLeft(Text.literal(speed).formatted(Formatting.GREEN)));
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/SkillLevelAdder.java b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/SkillLevelAdder.java
new file mode 100644
index 00000000..095982af
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/SkillLevelAdder.java
@@ -0,0 +1,34 @@
+package de.hysky.skyblocker.skyblock.item.slottext.adders;
+
+import de.hysky.skyblocker.skyblock.item.slottext.SlotText;
+import de.hysky.skyblocker.skyblock.item.slottext.SlotTextAdder;
+import de.hysky.skyblocker.utils.RomanNumerals;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+public class SkillLevelAdder extends SlotTextAdder {
+ public SkillLevelAdder() {
+ super("^Your Skills");
+ }
+
+ @Override
+ public @NotNull List<SlotText> getText(Slot slot) {
+ switch (slot.id) {
+ case 19, 20, 21, 22, 23, 24, 25, 29, 30, 31, 32 -> { //These are the slots that contain the skill items. Note that they aren't continuous, as there are 2 rows.
+ String name = slot.getStack().getName().getString();
+ int lastIndex = name.lastIndexOf(' ');
+ if (lastIndex == -1) return List.of(SlotText.bottomLeft(Text.literal("0").formatted(Formatting.LIGHT_PURPLE))); //Skills without any levels don't display any roman numerals. Probably because 0 doesn't exist.
+ String romanNumeral = name.substring(lastIndex + 1); //+1 because we don't need the space itself
+ //The "romanNumeral" might be a latin numeral, too. There's a skyblock setting for this, so we have to do it this way V
+ return List.of(SlotText.bottomLeft(Text.literal(String.valueOf(RomanNumerals.isValidRomanNumeral(romanNumeral) ? RomanNumerals.romanToDecimal(romanNumeral) : Integer.parseInt(romanNumeral))).formatted(Formatting.LIGHT_PURPLE)));
+ }
+ default -> {
+ return List.of();
+ }
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/SkyblockLevelAdder.java b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/SkyblockLevelAdder.java
new file mode 100644
index 00000000..8b528508
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/slottext/adders/SkyblockLevelAdder.java
@@ -0,0 +1,29 @@
+package de.hysky.skyblocker.skyblock.item.slottext.adders;
+
+import de.hysky.skyblocker.skyblock.item.slottext.SlotText;
+import de.hysky.skyblocker.skyblock.item.slottext.SlotTextAdder;
+import de.hysky.skyblocker.utils.ItemUtils;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.Text;
+import org.apache.commons.lang3.math.NumberUtils;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+public class SkyblockLevelAdder extends SlotTextAdder {
+ public SkyblockLevelAdder() {
+ super("^SkyBlock Menu");
+ }
+
+ @Override
+ public @NotNull List<SlotText> getText(Slot slot) {
+ if (slot.getIndex() != 22) return List.of();
+ List<Text> lore = ItemUtils.getLore(slot.getStack());
+ if (lore.isEmpty()) return List.of();
+ List<Text> siblings = lore.getFirst().getSiblings();
+ if (siblings.size() < 3) return List.of();
+ Text levelText = siblings.get(2); //The 3rd child is the level text itself
+ if (!NumberUtils.isDigits(levelText.getString())) return List.of();
+ return List.of(SlotText.bottomLeft(levelText));
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/AccessoriesHelper.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/AccessoriesHelper.java
index 8798a139..992206ad 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/AccessoriesHelper.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/AccessoriesHelper.java
@@ -108,7 +108,7 @@ public class AccessoriesHelper {
.put(page, new ObjectOpenHashSet<>(accessoryIds));
}
- static Pair<AccessoryReport, String> calculateReport4Accessory(String accessoryId) {
+ public static Pair<AccessoryReport, String> calculateReport4Accessory(String accessoryId) {
if (!ACCESSORY_DATA.containsKey(accessoryId) || Utils.getProfileId().isEmpty()) return Pair.of(AccessoryReport.INELIGIBLE, null);
Accessory accessory = ACCESSORY_DATA.get(accessoryId);
@@ -208,7 +208,7 @@ public class AccessoriesHelper {
}
}
- enum AccessoryReport {
+ public enum AccessoryReport {
HAS_HIGHEST_TIER, //You've collected the highest tier - Collected
IS_GREATER_TIER, //This accessory is an upgrade from the one in the same family that you already have - Upgrade -- Shows you what tier this accessory is in its family
HAS_GREATER_TIER, //This accessory has a higher tier upgrade - Upgradable -- Shows you the highest tier accessory you've collected in that family
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/CompactorDeletorPreview.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/CompactorDeletorPreview.java
index c5279c61..b93ca77a 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/CompactorDeletorPreview.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/CompactorDeletorPreview.java
@@ -7,7 +7,6 @@ import it.unimi.dsi.fastutil.ints.IntIntPair;
import it.unimi.dsi.fastutil.ints.IntObjectPair;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.DrawContext;
-import net.minecraft.client.gui.screen.Screen;
import net.minecraft.client.gui.tooltip.HoveredTooltipPositioner;
import net.minecraft.client.gui.tooltip.TooltipComponent;
import net.minecraft.item.ItemStack;
@@ -34,8 +33,7 @@ public class CompactorDeletorPreview {
public static final Pattern NAME = Pattern.compile("PERSONAL_(?<type>COMPACTOR|DELETOR)_(?<size>\\d+)");
private static final MinecraftClient client = MinecraftClient.getInstance();
- public static boolean drawPreview(DrawContext context, ItemStack stack, String type, String size, int x, int y) {
- List<Text> tooltips = Screen.getTooltipFromItem(client, stack);
+ public static boolean drawPreview(DrawContext context, ItemStack stack, List<Text> tooltips, String type, String size, int x, int y) {
int targetIndex = getTargetIndex(tooltips);
if (targetIndex == -1) return false;
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ExoticTooltip.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ExoticTooltip.java
deleted file mode 100644
index 46babc8b..00000000
--- a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ExoticTooltip.java
+++ /dev/null
@@ -1,96 +0,0 @@
-package de.hysky.skyblocker.skyblock.item.tooltip;
-
-import de.hysky.skyblocker.utils.Constants;
-import net.minecraft.nbt.NbtCompound;
-import net.minecraft.text.MutableText;
-import net.minecraft.text.Text;
-import net.minecraft.util.Formatting;
-import net.minecraft.util.StringIdentifiable;
-
-public class ExoticTooltip {
- public static String getExpectedHex(String id) {
- String color = TooltipInfoType.COLOR.getData().get(id).getAsString();
- if (color != null) {
- String[] RGBValues = color.split(",");
- return String.format("%02X%02X%02X", Integer.parseInt(RGBValues[0]), Integer.parseInt(RGBValues[1]), Integer.parseInt(RGBValues[2]));
- } else {
- ItemTooltip.LOGGER.warn("[Skyblocker Exotics] No expected color data found for id {}", id);
- return null;
- }
- }
-
- public static boolean isException(String id, String hex) {
- if (id.startsWith("LEATHER") || id.equals("GHOST_BOOTS") || Constants.SEYMOUR_IDS.contains(id)) {
- return true;
- }
- if (id.startsWith("RANCHER")) {
- return Constants.RANCHERS.contains(hex);
- }
- if (id.contains("ADAPTIVE_CHESTPLATE")) {
- return Constants.ADAPTIVE_CHEST.contains(hex);
- } else if (id.contains("ADAPTIVE")) {
- return Constants.ADAPTIVE.contains(hex);
- }
- if (id.startsWith("REAPER")) {
- return Constants.REAPER.contains(hex);
- }
- if (id.startsWith("FAIRY")) {
- return Constants.FAIRY_HEXES.contains(hex);
- }
- if (id.startsWith("CRYSTAL")) {
- return Constants.CRYSTAL_HEXES.contains(hex);
- }
- if (id.contains("SPOOK")) {
- return Constants.SPOOK.contains(hex);
- }
- return false;
- }
-
- public static DyeType checkDyeType(String hex) {
- if (Constants.CRYSTAL_HEXES.contains(hex)) {
- return DyeType.CRYSTAL;
- }
- if (Constants.FAIRY_HEXES.contains(hex)) {
- return DyeType.FAIRY;
- }
- if (Constants.OG_FAIRY_HEXES.contains(hex)) {
- return DyeType.OG_FAIRY;
- }
- if (Constants.SPOOK.contains(hex)) {
- return DyeType.SPOOK;
- }
- if (Constants.GLITCHED.contains(hex)) {
- return DyeType.GLITCHED;
- }
- return DyeType.EXOTIC;
- }
-
- public static boolean intendedDyed(NbtCompound customData) {
- return customData.contains("dye_item");
- }
-
- public enum DyeType implements StringIdentifiable {
- CRYSTAL("crystal", Formatting.AQUA),
- FAIRY("fairy", Formatting.LIGHT_PURPLE),
- OG_FAIRY("og_fairy", Formatting.DARK_PURPLE),
- SPOOK("spook", Formatting.RED),
- GLITCHED("glitched", Formatting.BLUE),
- EXOTIC("exotic", Formatting.GOLD);
- private final String name;
- private final Formatting formatting;
-
- DyeType(String name, Formatting formatting) {
- this.name = name;
- this.formatting = formatting;
- }
-
- @Override
- public String asString() {
- return name;
- }
-
- public MutableText getTranslatedText() {
- return Text.translatable("skyblocker.exotic." + name).formatted(formatting);
- }
- }
-}
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 8c083e25..e6a364e4 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
@@ -1,259 +1,28 @@
package de.hysky.skyblocker.skyblock.item.tooltip;
-import com.google.gson.JsonObject;
-import de.hysky.skyblocker.SkyblockerMod;
import de.hysky.skyblocker.config.SkyblockerConfigManager;
import de.hysky.skyblocker.config.configs.GeneralConfig;
-import de.hysky.skyblocker.skyblock.item.MuseumItemCache;
-import de.hysky.skyblocker.skyblock.item.tooltip.AccessoriesHelper.AccessoryReport;
import de.hysky.skyblocker.utils.Constants;
-import de.hysky.skyblocker.utils.ItemUtils;
import de.hysky.skyblocker.utils.Utils;
import de.hysky.skyblocker.utils.scheduler.Scheduler;
-import it.unimi.dsi.fastutil.Pair;
import net.minecraft.client.MinecraftClient;
-import net.minecraft.client.item.TooltipType;
-import net.minecraft.component.DataComponentTypes;
-import net.minecraft.component.type.DyedColorComponent;
-import net.minecraft.item.Item;
-import net.minecraft.item.ItemStack;
-import net.minecraft.nbt.NbtCompound;
-import net.minecraft.nbt.NbtElement;
-import net.minecraft.text.MutableText;
import net.minecraft.text.Text;
import net.minecraft.util.Formatting;
-
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
import java.util.concurrent.CompletableFuture;
public class ItemTooltip {
protected static final Logger LOGGER = LoggerFactory.getLogger(ItemTooltip.class.getName());
private static final MinecraftClient client = MinecraftClient.getInstance();
- protected static final GeneralConfig.ItemTooltip config = SkyblockerConfigManager.get().general.itemTooltip;
+ public static final GeneralConfig.ItemTooltip config = SkyblockerConfigManager.get().general.itemTooltip;
private static volatile boolean sentNullWarning = false;
- public static void getTooltip(ItemStack stack, Item.TooltipContext tooltipContext, TooltipType tooltipType, List<Text> lines) {
- if (!Utils.isOnSkyblock() || client.player == null) return;
-
- smoothenLines(lines);
-
- String name = getInternalNameFromNBT(stack, false);
- String internalID = getInternalNameFromNBT(stack, true);
- String neuName = name;
- if (name == null || internalID == null) return;
-
- if (name.startsWith("ISSHINY_")) {
- name = "SHINY_" + internalID;
- neuName = internalID;
- }
-
- if (lines.isEmpty()) {
- return;
- }
-
- int count = stack.getCount();
- boolean bazaarOpened = lines.stream().anyMatch(each -> each.getString().contains("Buy price:") || each.getString().contains("Sell price:"));
-
- if (TooltipInfoType.NPC.isTooltipEnabledAndHasOrNullWarning(internalID)) {
- lines.add(Text.literal(String.format("%-21s", "NPC Sell Price:"))
- .formatted(Formatting.YELLOW)
- .append(getCoinsMessage(TooltipInfoType.NPC.getData().get(internalID).getAsDouble(), count)));
- }
-
- boolean bazaarExist = false;
-
- if (TooltipInfoType.BAZAAR.isTooltipEnabledAndHasOrNullWarning(name) && !bazaarOpened) {
- JsonObject getItem = TooltipInfoType.BAZAAR.getData().getAsJsonObject(name);
- lines.add(Text.literal(String.format("%-18s", "Bazaar buy Price:"))
- .formatted(Formatting.GOLD)
- .append(getItem.get("buyPrice").isJsonNull()
- ? Text.literal("No data").formatted(Formatting.RED)
- : getCoinsMessage(getItem.get("buyPrice").getAsDouble(), count)));
- lines.add(Text.literal(String.format("%-19s", "Bazaar sell Price:"))
- .formatted(Formatting.GOLD)
- .append(getItem.get("sellPrice").isJsonNull()
- ? Text.literal("No data").formatted(Formatting.RED)
- : getCoinsMessage(getItem.get("sellPrice").getAsDouble(), count)));
- bazaarExist = true;
- }
-
- // bazaarOpened & bazaarExist check for lbin, because Skytils keeps some bazaar item data in lbin api
- boolean lbinExist = false;
- if (TooltipInfoType.LOWEST_BINS.isTooltipEnabledAndHasOrNullWarning(name) && !bazaarOpened && !bazaarExist) {
- lines.add(Text.literal(String.format("%-19s", "Lowest BIN Price:"))
- .formatted(Formatting.GOLD)
- .append(getCoinsMessage(TooltipInfoType.LOWEST_BINS.getData().get(name).getAsDouble(), count)));
- lbinExist = true;
- }
-
- if (SkyblockerConfigManager.get().general.itemTooltip.enableAvgBIN) {
- if (TooltipInfoType.ONE_DAY_AVERAGE.getData() == null || TooltipInfoType.THREE_DAY_AVERAGE.getData() == null) {
- nullWarning();
- } else {
- /*
- We are skipping check average prices for potions, runes
- and enchanted books because there is no data for their in API.
- */
- neuName = getNeuName(internalID, neuName);
-
- if (!neuName.isEmpty() && lbinExist) {
- GeneralConfig.Average type = config.avg;
-
- // "No data" line because of API not keeping old data, it causes NullPointerException
- if (type == GeneralConfig.Average.ONE_DAY || type == GeneralConfig.Average.BOTH) {
- lines.add(
- Text.literal(String.format("%-19s", "1 Day Avg. Price:"))
- .formatted(Formatting.GOLD)
- .append(TooltipInfoType.ONE_DAY_AVERAGE.getData().get(neuName) == null
- ? Text.literal("No data").formatted(Formatting.RED)
- : getCoinsMessage(TooltipInfoType.ONE_DAY_AVERAGE.getData().get(neuName).getAsDouble(), count)
- )
- );
- }
- if (type == GeneralConfig.Average.THREE_DAY || type == GeneralConfig.Average.BOTH) {
- lines.add(
- Text.literal(String.format("%-19s", "3 Day Avg. Price:"))
- .formatted(Formatting.GOLD)
- .append(TooltipInfoType.THREE_DAY_AVERAGE.getData().get(neuName) == null
- ? Text.literal("No data").formatted(Formatting.RED)
- : getCoinsMessage(TooltipInfoType.THREE_DAY_AVERAGE.getData().get(neuName).getAsDouble(), count)
- )
- );
- }
- }
- }
- }
-
- 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"
- );
-
- if (SkyblockerConfigManager.get().general.itemTooltip.dungeonQuality) {
- NbtCompound customData = ItemUtils.getCustomData(stack);
- if (customData != null && customData.contains("baseStatBoostPercentage")) {
- int baseStatBoostPercentage = customData.getInt("baseStatBoostPercentage");
- boolean maxQuality = baseStatBoostPercentage == 50;
- if (maxQuality) {
- lines.add(Text.literal(String.format("%-17s", "Item Quality:") + baseStatBoostPercentage + "/50").formatted(Formatting.RED).formatted(Formatting.BOLD));
- } else {
- lines.add(Text.literal(String.format("%-21s", "Item Quality:") + baseStatBoostPercentage + "/50").formatted(Formatting.BLUE));
- }
- if (customData.contains("item_tier")) { // sometimes it just isn't here?
- int itemTier = customData.getInt("item_tier");
- if (maxQuality) {
- lines.add(Text.literal(String.format("%-17s", "Floor Tier:") + itemTier + " (" + itemTierFloors.get(itemTier) + ")").formatted(Formatting.RED).formatted(Formatting.BOLD));
- } else {
- lines.add(Text.literal(String.format("%-21s", "Floor Tier:") + itemTier + " (" + itemTierFloors.get(itemTier) + ")").formatted(Formatting.BLUE));
- }
- }
- }
- }
-
- if (TooltipInfoType.MOTES.isTooltipEnabledAndHasOrNullWarning(internalID)) {
- lines.add(Text.literal(String.format("%-20s", "Motes Price:"))
- .formatted(Formatting.LIGHT_PURPLE)
- .append(getMotesMessage(TooltipInfoType.MOTES.getData().get(internalID).getAsInt(), count)));
- }
-
- if (TooltipInfoType.OBTAINED.isTooltipEnabled()) {
- String timestamp = ItemUtils.getTimestamp(stack);
-
- if (!timestamp.isEmpty()) {
- lines.add(Text.literal(String.format("%-21s", "Obtained: "))
- .formatted(Formatting.LIGHT_PURPLE)
- .append(Text.literal(timestamp).formatted(Formatting.RED)));
- }
- }
-
- if (TooltipInfoType.MUSEUM.isTooltipEnabledAndHasOrNullWarning(internalID) && !bazaarOpened) {
- String itemCategory = TooltipInfoType.MUSEUM.getData().get(internalID).getAsString();
- String format = switch (itemCategory) {
- case "Weapons" -> "%-18s";
- case "Armor" -> "%-19s";
- default -> "%-20s";
- };
-
- //Special case the special category so that it doesn't always display not donated
- if (itemCategory.equals("Special")) {
- lines.add(Text.literal(String.format(format, "Museum: (" + itemCategory + ")"))
- .formatted(Formatting.LIGHT_PURPLE));
- } else {
- NbtCompound customData = ItemUtils.getCustomData(stack);
- boolean isInMuseum = (customData.contains("donated_museum") && customData.getBoolean("donated_museum")) || MuseumItemCache.hasItemInMuseum(internalID);
-
- Formatting donatedIndicatorFormatting = isInMuseum ? Formatting.GREEN : Formatting.RED;
-
- lines.add(Text.literal(String.format(format, "Museum (" + itemCategory + "):"))
- .formatted(Formatting.LIGHT_PURPLE)
- .append(Text.literal(isInMuseum ? "✔" : "✖").formatted(donatedIndicatorFormatting, Formatting.BOLD))
- .append(Text.literal(isInMuseum ? " Donated" : " Not Donated").formatted(donatedIndicatorFormatting)));
- }
- }
-
- if (TooltipInfoType.COLOR.isTooltipEnabledAndHasOrNullWarning(internalID) && stack.contains(DataComponentTypes.DYED_COLOR)) {
- String uuid = ItemUtils.getItemUuid(stack);
- boolean hasCustomDye = SkyblockerConfigManager.get().general.customDyeColors.containsKey(uuid) || SkyblockerConfigManager.get().general.customAnimatedDyes.containsKey(uuid);
- //DyedColorComponent#getColor returns ARGB so we mask out the alpha bits
- int dyeColor = DyedColorComponent.getColor(stack, 0);
-
- // dyeColor will have alpha = 255 if it's dyed, and alpha = 0 if it's not dyed,
- if (!hasCustomDye && dyeColor != 0) {
- dyeColor = dyeColor & 0x00FFFFFF;
- String colorHex = String.format("%06X", dyeColor);
- String expectedHex = ExoticTooltip.getExpectedHex(internalID);
-
- boolean correctLine = false;
- for (Text text : lines) {
- String existingTooltip = text.getString() + " ";
- if (existingTooltip.startsWith("Color: ")) {
- correctLine = true;
-
- addExoticTooltip(lines, internalID, ItemUtils.getCustomData(stack), colorHex, expectedHex, existingTooltip);
- break;
- }
- }
-
- if (!correctLine) {
- addExoticTooltip(lines, internalID, ItemUtils.getCustomData(stack), colorHex, expectedHex, "");
- }
- }
- }
-
- if (TooltipInfoType.ACCESSORIES.isTooltipEnabledAndHasOrNullWarning(internalID)) {
- Pair<AccessoryReport, String> report = AccessoriesHelper.calculateReport4Accessory(internalID);
-
- if (report.left() != AccessoryReport.INELIGIBLE) {
- MutableText title = Text.literal(String.format("%-19s", "Accessory: ")).withColor(0xf57542);
-
- Text stateText = switch (report.left()) {
- case HAS_HIGHEST_TIER -> Text.literal("✔ Collected").formatted(Formatting.GREEN);
- case IS_GREATER_TIER -> Text.literal("✦ Upgrade ").withColor(0x218bff).append(Text.literal(report.right()).withColor(0xf8f8ff));
- case HAS_GREATER_TIER -> Text.literal("↑ Upgradable ").withColor(0xf8d048).append(Text.literal(report.right()).withColor(0xf8f8ff));
- case OWNS_BETTER_TIER -> Text.literal("↓ Downgrade ").formatted(Formatting.GRAY).append(Text.literal(report.right()).withColor(0xf8f8ff));
- case MISSING -> Text.literal("✖ Missing ").formatted(Formatting.RED).append(Text.literal(report.right()).withColor(0xf8f8ff));
-
- //Should never be the case
- default -> Text.literal("? Unknown").formatted(Formatting.GRAY);
- };
-
- lines.add(title.append(stateText));
- }
- }
- }
-
@NotNull
public static String getNeuName(String internalID, String neuName) {
switch (internalID) {
@@ -280,13 +49,6 @@ public class ItemTooltip {
return neuName;
}
- private static void addExoticTooltip(List<Text> lines, String internalID, NbtCompound customData, String colorHex, String expectedHex, String existingTooltip) {
- if (expectedHex != null && !colorHex.equalsIgnoreCase(expectedHex) && !ExoticTooltip.isException(internalID, colorHex) && !ExoticTooltip.intendedDyed(customData)) {
- final ExoticTooltip.DyeType type = ExoticTooltip.checkDyeType(colorHex);
- lines.add(1, Text.literal(existingTooltip + Formatting.DARK_GRAY + "(").append(type.getTranslatedText()).append(Formatting.DARK_GRAY + ")"));
- }
- }
-
public static void nullWarning() {
if (!sentNullWarning && client.player != null) {
LOGGER.warn(Constants.PREFIX.get().append(Text.translatable("skyblocker.itemTooltip.nullMessage")).getString());
@@ -294,69 +56,7 @@ public class ItemTooltip {
}
}
- // TODO What in the world is this?
- public static String getInternalNameFromNBT(ItemStack stack, boolean internalIDOnly) {
- NbtCompound customData = ItemUtils.getCustomData(stack);
-
- if (customData == null || !customData.contains(ItemUtils.ID, NbtElement.STRING_TYPE)) {
- return null;
- }
- String internalName = customData.getString(ItemUtils.ID);
-
- if (internalIDOnly) {
- return internalName;
- }
-
- // Transformation to API format.
- if (customData.contains("is_shiny")) {
- return "ISSHINY_" + internalName;
- }
-
- switch (internalName) {
- case "ENCHANTED_BOOK" -> {
- if (customData.contains("enchantments")) {
- NbtCompound enchants = customData.getCompound("enchantments");
- Optional<String> firstEnchant = enchants.getKeys().stream().findFirst();
- String enchant = firstEnchant.orElse("");
- return "ENCHANTMENT_" + enchant.toUpperCase(Locale.ENGLISH) + "_" + enchants.getInt(enchant);
- }
- }
- case "PET" -> {
- if (customData.contains("petInfo")) {
- JsonObject petInfo = SkyblockerMod.GSON.fromJson(customData.getString("petInfo"), JsonObject.class);
- return "LVL_1_" + petInfo.get("tier").getAsString() + "_" + petInfo.get("type").getAsString();
- }
- }
- case "POTION" -> {
- String enhanced = customData.contains("enhanced") ? "_ENHANCED" : "";
- String extended = customData.contains("extended") ? "_EXTENDED" : "";
- String splash = customData.contains("splash") ? "_SPLASH" : "";
- if (customData.contains("potion") && customData.contains("potion_level")) {
- return (customData.getString("potion") + "_" + internalName + "_" + customData.getInt("potion_level")
- + enhanced + extended + splash).toUpperCase(Locale.ENGLISH);
- }
- }
- case "RUNE" -> {
- if (customData.contains("runes")) {
- NbtCompound runes = customData.getCompound("runes");
- Optional<String> firstRunes = runes.getKeys().stream().findFirst();
- String rune = firstRunes.orElse("");
- return rune.toUpperCase(Locale.ENGLISH) + "_RUNE_" + runes.getInt(rune);
- }
- }
- case "ATTRIBUTE_SHARD" -> {
- if (customData.contains("attributes")) {
- NbtCompound shards = customData.getCompound("attributes");
- Optional<String> firstShards = shards.getKeys().stream().findFirst();
- String shard = firstShards.orElse("");
- return internalName + "-" + shard.toUpperCase(Locale.ENGLISH) + "_" + shards.getInt(shard);
- }
- }
- }
- return internalName;
- }
-
- private static Text getCoinsMessage(double price, int count) {
+ public static Text getCoinsMessage(double price, int count) {
// Format the price string once
String priceString = String.format(Locale.ENGLISH, "%1$,.1f", price);
@@ -367,47 +67,9 @@ public class ItemTooltip {
// If count is greater than 1, include the "each" information
String priceStringTotal = String.format(Locale.ENGLISH, "%1$,.1f", price * count);
- MutableText message = Text.literal(priceStringTotal + " Coins ").formatted(Formatting.DARK_AQUA);
- message.append(Text.literal("(" + priceString + " each)").formatted(Formatting.GRAY));
-
- return message;
- }
-
- private static Text getMotesMessage(int price, int count) {
- float motesMultiplier = SkyblockerConfigManager.get().otherLocations.rift.mcGrubberStacks * 0.05f + 1;
-
- // Calculate the total price
- int totalPrice = price * count;
- String totalPriceString = String.format(Locale.ENGLISH, "%1$,.1f", totalPrice * motesMultiplier);
-
- // If count is 1, return a simple message
- if (count == 1) {
- return Text.literal(totalPriceString.replace(".0", "") + " Motes").formatted(Formatting.DARK_AQUA);
- }
-
- // If count is greater than 1, include the "each" information
- String eachPriceString = String.format(Locale.ENGLISH, "%1$,.1f", price * motesMultiplier);
- MutableText message = Text.literal(totalPriceString.replace(".0", "") + " Motes ").formatted(Formatting.DARK_AQUA);
- message.append(Text.literal("(" + eachPriceString.replace(".0", "") + " each)").formatted(Formatting.GRAY));
-
- 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++) {
- 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);
+ return Text.literal(priceStringTotal + " Coins ").formatted(Formatting.DARK_AQUA)
+ .append(Text.literal("(" + priceString + " each)").formatted(Formatting.GRAY));
}
// If these options is true beforehand, the client will get first data of these options while loading.
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipAdder.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipAdder.java
new file mode 100644
index 00000000..9bd63adc
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipAdder.java
@@ -0,0 +1,45 @@
+package de.hysky.skyblocker.skyblock.item.tooltip;
+
+import de.hysky.skyblocker.utils.render.gui.AbstractContainerMatcher;
+import net.minecraft.item.ItemStack;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.Text;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+import java.util.regex.Pattern;
+
+/**
+ * Extend this class and add it to {@link TooltipManager#adders} to add additional text to tooltips.
+ */
+public abstract class TooltipAdder extends AbstractContainerMatcher {
+ /**
+ * The priority of this adder. Lower priority means it will be applied first.
+ * @apiNote Consider taking this value on your class' constructor and setting it from {@link TooltipManager#adders} to make it easy to read and maintain.
+ */
+ public final int priority;
+
+ protected TooltipAdder(String titlePattern, int priority) {
+ super(titlePattern);
+ this.priority = priority;
+ }
+
+ protected TooltipAdder(Pattern titlePattern, int priority) {
+ super(titlePattern);
+ this.priority = priority;
+ }
+
+ /**
+ * Creates a TooltipAdder that will be applied to all screens.
+ */
+ protected TooltipAdder(int priority) {
+ super();
+ this.priority = priority;
+ }
+
+ /**
+ * @implNote The first element of the lines list holds the item's display name,
+ * as it's a list of all lines that will be displayed in the tooltip.
+ */
+ public abstract void addToTooltip(@Nullable Slot focusedSlot, ItemStack stack, List<Text> lines);
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipManager.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipManager.java
new file mode 100644
index 00000000..e3a2ef04
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipManager.java
@@ -0,0 +1,87 @@
+package de.hysky.skyblocker.skyblock.item.tooltip;
+
+import de.hysky.skyblocker.mixins.accessors.HandledScreenAccessor;
+import de.hysky.skyblocker.skyblock.chocolatefactory.ChocolateFactorySolver;
+import de.hysky.skyblocker.skyblock.item.tooltip.adders.*;
+import de.hysky.skyblocker.utils.Utils;
+import net.fabricmc.fabric.api.client.item.v1.ItemTooltipCallback;
+import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.screen.Screen;
+import net.minecraft.client.gui.screen.ingame.HandledScreen;
+import net.minecraft.item.ItemStack;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.Text;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+public class TooltipManager {
+ private static final TooltipAdder[] adders = new TooltipAdder[]{
+ new LineSmoothener(), // Applies before anything else
+ new SupercraftReminder(),
+ new ChocolateFactorySolver.Tooltip(),
+ new NpcPriceTooltip(1),
+ new BazaarPriceTooltip(2),
+ new LBinTooltip(3),
+ new AvgBinTooltip(4),
+ new DungeonQualityTooltip(5),
+ new MotesTooltip(6),
+ new ObtainedDateTooltip(7),
+ new MuseumTooltip(8),
+ new ColorTooltip(9),
+ new AccessoryTooltip(10),
+ };
+ private static final ArrayList<TooltipAdder> currentScreenAdders = new ArrayList<>();
+
+ private TooltipManager() {
+ }
+
+ public static void init() {
+ ItemTooltipCallback.EVENT.register((stack, tooltipContext, tooltipType, lines) -> {
+ if (MinecraftClient.getInstance().currentScreen instanceof HandledScreen<?> handledScreen) {
+ addToTooltip(((HandledScreenAccessor) handledScreen).getFocusedSlot(), stack, lines);
+ } else {
+ addToTooltip(null, stack, lines);
+ }
+ });
+ ScreenEvents.AFTER_INIT.register((client, screen, width, height) -> {
+ onScreenChange(screen);
+ ScreenEvents.remove(screen).register(ignored -> currentScreenAdders.clear());
+ });
+ }
+
+ private static void onScreenChange(Screen screen) {
+ final String title = screen.getTitle().getString();
+ currentScreenAdders.clear();
+ for (TooltipAdder adder : adders) {
+ if (adder.titlePattern == null || adder.titlePattern.matcher(title).find()) {
+ currentScreenAdders.add(adder);
+ }
+ }
+ currentScreenAdders.sort(Comparator.comparingInt(adder -> adder.priority));
+ }
+
+ /**
+ * <p>Adds additional text from all adders that are applicable to the current screen.
+ * This method is run on each tooltip render, so don't do any heavy calculations here.</p>
+ *
+ * <p>If you want to add info to the tooltips of multiple items, consider using a switch statement with {@code focusedSlot.getIndex()}</p>
+ *
+ * @param focusedSlot The slot that is currently focused by the cursor.
+ * @param stack The stack to render the tooltip for.
+ * @param lines The tooltip lines of the focused item. This includes the display name, as it's a part of the tooltip (at index 0).
+ * @return The lines list itself after all adders have added their text.
+ * @deprecated This method is public only for the sake of the mixin. Don't call directly, not that there is any point to it.
+ */
+ @Deprecated
+ public static List<Text> addToTooltip(@Nullable Slot focusedSlot, ItemStack stack, List<Text> lines) {
+ if (!Utils.isOnSkyblock()) return lines;
+ for (TooltipAdder adder : currentScreenAdders) {
+ adder.addToTooltip(focusedSlot, stack, lines);
+ }
+ return lines;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/AccessoryTooltip.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/AccessoryTooltip.java
new file mode 100644
index 00000000..caed0e0e
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/AccessoryTooltip.java
@@ -0,0 +1,45 @@
+package de.hysky.skyblocker.skyblock.item.tooltip.adders;
+
+import de.hysky.skyblocker.skyblock.item.tooltip.AccessoriesHelper;
+import de.hysky.skyblocker.skyblock.item.tooltip.TooltipAdder;
+import de.hysky.skyblocker.skyblock.item.tooltip.TooltipInfoType;
+import it.unimi.dsi.fastutil.Pair;
+import net.minecraft.item.ItemStack;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+public class AccessoryTooltip extends TooltipAdder {
+ public AccessoryTooltip(int priority) {
+ super(priority);
+ }
+
+ @Override
+ public void addToTooltip(@Nullable Slot focusedSlot, ItemStack stack, List<Text> lines) {
+ final String internalID = stack.getSkyblockId();
+ if (TooltipInfoType.ACCESSORIES.isTooltipEnabledAndHasOrNullWarning(internalID)) {
+ Pair<AccessoriesHelper.AccessoryReport, String> report = AccessoriesHelper.calculateReport4Accessory(internalID);
+
+ if (report.left() != AccessoriesHelper.AccessoryReport.INELIGIBLE) {
+ MutableText title = Text.literal(String.format("%-19s", "Accessory: ")).withColor(0xf57542);
+
+ Text stateText = switch (report.left()) {
+ case HAS_HIGHEST_TIER -> Text.literal("✔ Collected").formatted(Formatting.GREEN);
+ case IS_GREATER_TIER -> Text.literal("✦ Upgrade ").withColor(0x218bff).append(Text.literal(report.right()).withColor(0xf8f8ff));
+ case HAS_GREATER_TIER -> Text.literal("↑ Upgradable ").withColor(0xf8d048).append(Text.literal(report.right()).withColor(0xf8f8ff));
+ case OWNS_BETTER_TIER -> Text.literal("↓ Downgrade ").formatted(Formatting.GRAY).append(Text.literal(report.right()).withColor(0xf8f8ff));
+ case MISSING -> Text.literal("✖ Missing ").formatted(Formatting.RED).append(Text.literal(report.right()).withColor(0xf8f8ff));
+
+ //Should never be the case
+ default -> Text.literal("? Unknown").formatted(Formatting.GRAY);
+ };
+
+ lines.add(title.append(stateText));
+ }
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/AvgBinTooltip.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/AvgBinTooltip.java
new file mode 100644
index 00000000..d7a56b95
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/AvgBinTooltip.java
@@ -0,0 +1,63 @@
+package de.hysky.skyblocker.skyblock.item.tooltip.adders;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.config.configs.GeneralConfig;
+import de.hysky.skyblocker.skyblock.item.tooltip.ItemTooltip;
+import de.hysky.skyblocker.skyblock.item.tooltip.TooltipAdder;
+import de.hysky.skyblocker.skyblock.item.tooltip.TooltipInfoType;
+import net.minecraft.item.ItemStack;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+public class AvgBinTooltip extends TooltipAdder {
+ public AvgBinTooltip(int priority) {
+ super(priority);
+ }
+
+ @Override
+ public void addToTooltip(@Nullable Slot focusedSlot, ItemStack stack, List<Text> lines) {
+ String neuName = stack.getNeuName();
+ String internalID = stack.getSkyblockId();
+ if (neuName == null || internalID == null) return;
+
+ if (SkyblockerConfigManager.get().general.itemTooltip.enableAvgBIN) {
+ if (TooltipInfoType.ONE_DAY_AVERAGE.getData() == null || TooltipInfoType.THREE_DAY_AVERAGE.getData() == null) {
+ ItemTooltip.nullWarning();
+ } else {
+ /*
+ We are skipping check average prices for potions, runes
+ and enchanted books because there is no data for their in API.
+ */
+ if (!neuName.isEmpty() && LBinTooltip.lbinExist) {
+ GeneralConfig.Average type = ItemTooltip.config.avg;
+
+ // "No data" line because of API not keeping old data, it causes NullPointerException
+ if (type == GeneralConfig.Average.ONE_DAY || type == GeneralConfig.Average.BOTH) {
+ lines.add(
+ Text.literal(String.format("%-19s", "1 Day Avg. Price:"))
+ .formatted(Formatting.GOLD)
+ .append(TooltipInfoType.ONE_DAY_AVERAGE.getData().get(neuName) == null
+ ? Text.literal("No data").formatted(Formatting.RED)
+ : ItemTooltip.getCoinsMessage(TooltipInfoType.ONE_DAY_AVERAGE.getData().get(neuName).getAsDouble(), stack.getCount())
+ )
+ );
+ }
+ if (type == GeneralConfig.Average.THREE_DAY || type == GeneralConfig.Average.BOTH) {
+ lines.add(
+ Text.literal(String.format("%-19s", "3 Day Avg. Price:"))
+ .formatted(Formatting.GOLD)
+ .append(TooltipInfoType.THREE_DAY_AVERAGE.getData().get(neuName) == null
+ ? Text.literal("No data").formatted(Formatting.RED)
+ : ItemTooltip.getCoinsMessage(TooltipInfoType.THREE_DAY_AVERAGE.getData().get(neuName).getAsDouble(), stack.getCount())
+ )
+ );
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/BazaarPriceTooltip.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/BazaarPriceTooltip.java
new file mode 100644
index 00000000..d2fa563b
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/BazaarPriceTooltip.java
@@ -0,0 +1,57 @@
+package de.hysky.skyblocker.skyblock.item.tooltip.adders;
+
+import com.google.gson.JsonObject;
+import de.hysky.skyblocker.skyblock.item.tooltip.ItemTooltip;
+import de.hysky.skyblocker.skyblock.item.tooltip.TooltipAdder;
+import de.hysky.skyblocker.skyblock.item.tooltip.TooltipInfoType;
+import net.minecraft.item.ItemStack;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import org.apache.commons.lang3.math.NumberUtils;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+public class BazaarPriceTooltip extends TooltipAdder {
+ public static boolean bazaarExist = false;
+
+ public BazaarPriceTooltip(int priority) {
+ super(priority);
+ }
+
+ @Override
+ public void addToTooltip(@Nullable Slot focusedSlot, ItemStack stack, List<Text> lines) {
+ bazaarExist = false;
+ final String internalID = stack.getSkyblockId();
+ if (internalID == null) return;
+ String name = stack.getSkyblockApiId();
+ if (name == null) return;
+
+ if (name.startsWith("ISSHINY_")) name = "SHINY_" + internalID;
+
+ if (TooltipInfoType.BAZAAR.isTooltipEnabledAndHasOrNullWarning(name)) {
+ int amount;
+ if (lines.get(1).getString().endsWith("Sack")) {
+ //The amount is in the 2nd sibling of the 3rd line of the lore. here V
+ //Example line: empty[style={color=dark_purple,!italic}, siblings=[literal{Stored: }[style={color=gray}], literal{0}[style={color=dark_gray}], literal{/20k}[style={color=gray}]]
+ String line = lines.get(3).getSiblings().get(1).getString().replace(",", "");
+ amount = NumberUtils.isParsable(line) && !line.equals("0") ? Integer.parseInt(line) : stack.getCount();
+ } else {
+ amount = stack.getCount();
+ }
+ JsonObject getItem = TooltipInfoType.BAZAAR.getData().getAsJsonObject(name);
+ lines.add(Text.literal(String.format("%-18s", "Bazaar buy Price:"))
+ .formatted(Formatting.GOLD)
+ .append(getItem.get("buyPrice").isJsonNull()
+ ? Text.literal("No data").formatted(Formatting.RED)
+ : ItemTooltip.getCoinsMessage(getItem.get("buyPrice").getAsDouble(), amount)));
+ lines.add(Text.literal(String.format("%-19s", "Bazaar sell Price:"))
+ .formatted(Formatting.GOLD)
+ .append(getItem.get("sellPrice").isJsonNull()
+ ? Text.literal("No data").formatted(Formatting.RED)
+ : ItemTooltip.getCoinsMessage(getItem.get("sellPrice").getAsDouble(), amount)));
+ bazaarExist = true;
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/ColorTooltip.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/ColorTooltip.java
new file mode 100644
index 00000000..26a040ec
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/ColorTooltip.java
@@ -0,0 +1,135 @@
+package de.hysky.skyblocker.skyblock.item.tooltip.adders;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.skyblock.item.tooltip.TooltipAdder;
+import de.hysky.skyblocker.skyblock.item.tooltip.TooltipInfoType;
+import de.hysky.skyblocker.utils.Constants;
+import de.hysky.skyblocker.utils.ItemUtils;
+import net.minecraft.component.DataComponentTypes;
+import net.minecraft.component.type.DyedColorComponent;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.NbtCompound;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import net.minecraft.util.StringIdentifiable;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+
+public class ColorTooltip extends TooltipAdder {
+ private static final Logger LOGGER = LoggerFactory.getLogger(ColorTooltip.class);
+
+ public ColorTooltip(int priority) {
+ super(priority);
+ }
+
+ @Override
+ public void addToTooltip(@Nullable Slot focusedSlot, ItemStack stack, List<Text> lines) {
+ final String internalID = stack.getSkyblockId();
+ if (TooltipInfoType.COLOR.isTooltipEnabledAndHasOrNullWarning(internalID) && stack.contains(DataComponentTypes.DYED_COLOR)) {
+ String uuid = ItemUtils.getItemUuid(stack);
+ boolean hasCustomDye = SkyblockerConfigManager.get().general.customDyeColors.containsKey(uuid) || SkyblockerConfigManager.get().general.customAnimatedDyes.containsKey(uuid);
+ //DyedColorComponent#getColor returns ARGB so we mask out the alpha bits
+ int dyeColor = DyedColorComponent.getColor(stack, 0);
+
+ // dyeColor will have alpha = 255 if it's dyed, and alpha = 0 if it's not dyed,
+ if (!hasCustomDye && dyeColor != 0) {
+ dyeColor = dyeColor & 0x00FFFFFF;
+ String colorHex = String.format("%06X", dyeColor);
+ String expectedHex = getExpectedHex(internalID);
+
+ boolean correctLine = false;
+ for (Text text : lines) {
+ String existingTooltip = text.getString() + " ";
+ if (existingTooltip.startsWith("Color: ")) {
+ correctLine = true;
+
+ addExoticTooltip(lines, internalID, ItemUtils.getCustomData(stack), colorHex, expectedHex, existingTooltip);
+ break;
+ }
+ }
+
+ if (!correctLine) {
+ addExoticTooltip(lines, internalID, ItemUtils.getCustomData(stack), colorHex, expectedHex, "");
+ }
+ }
+ }
+ }
+
+ private static void addExoticTooltip(List<Text> lines, String internalID, NbtCompound customData, String colorHex, String expectedHex, String existingTooltip) {
+ if (expectedHex != null && !colorHex.equalsIgnoreCase(expectedHex) && !isException(internalID, colorHex) && !intendedDyed(customData)) {
+ final DyeType type = checkDyeType(colorHex);
+ lines.add(1, Text.literal(existingTooltip + Formatting.DARK_GRAY + "(").append(type.getTranslatedText()).append(Formatting.DARK_GRAY + ")"));
+ }
+ }
+
+ public static String getExpectedHex(String id) {
+ String color = TooltipInfoType.COLOR.getData().get(id).getAsString();
+ if (color != null) {
+ String[] RGBValues = color.split(",");
+ return String.format("%02X%02X%02X", Integer.parseInt(RGBValues[0]), Integer.parseInt(RGBValues[1]), Integer.parseInt(RGBValues[2]));
+ } else {
+ LOGGER.warn("[Skyblocker Exotics] No expected color data found for id {}", id);
+ return null;
+ }
+ }
+
+ public static boolean isException(String id, String hex) {
+ return switch (id) {
+ case String it when it.startsWith("LEATHER") || it.equals("GHOST_BOOTS") || Constants.SEYMOUR_IDS.contains(it) -> true;
+ case String it when it.startsWith("RANCHER") -> Constants.RANCHERS.contains(hex);
+ case String it when it.contains("ADAPTIVE_CHESTPLATE") -> Constants.ADAPTIVE_CHEST.contains(hex);
+ case String it when it.contains("ADAPTIVE") -> Constants.ADAPTIVE.contains(hex);
+ case String it when it.contains("REAPER") -> Constants.REAPER.contains(hex);
+ case String it when it.contains("FAIRY") -> Constants.FAIRY_HEXES.contains(hex);
+ case String it when it.contains("CRYSTAL") -> Constants.CRYSTAL_HEXES.contains(hex);
+ case String it when it.contains("SPOOK") -> Constants.SPOOK.contains(hex);
+ default -> false;
+ };
+ }
+
+ public static DyeType checkDyeType(String hex) {
+ return switch (hex) {
+ case String it when Constants.CRYSTAL_HEXES.contains(it) -> DyeType.CRYSTAL;
+ case String it when Constants.FAIRY_HEXES.contains(it) -> DyeType.FAIRY;
+ case String it when Constants.OG_FAIRY_HEXES.contains(it) -> DyeType.OG_FAIRY;
+ case String it when Constants.SPOOK.contains(it) -> DyeType.SPOOK;
+ case String it when Constants.GLITCHED.contains(it) -> DyeType.GLITCHED;
+ default -> DyeType.EXOTIC;
+ };
+ }
+
+ public static boolean intendedDyed(NbtCompound customData) {
+ return customData.contains("dye_item");
+ }
+
+ public enum DyeType implements StringIdentifiable {
+ CRYSTAL("crystal", Formatting.AQUA),
+ FAIRY("fairy", Formatting.LIGHT_PURPLE),
+ OG_FAIRY("og_fairy", Formatting.DARK_PURPLE),
+ SPOOK("spook", Formatting.RED),
+ GLITCHED("glitched", Formatting.BLUE),
+ EXOTIC("exotic", Formatting.GOLD);
+ private final String name;
+ private final Formatting formatting;
+
+ DyeType(String name, Formatting formatting) {
+ this.name = name;
+ this.formatting = formatting;
+ }
+
+ @Override
+ public String asString() {
+ return name;
+ }
+
+ public MutableText getTranslatedText() {
+ return Text.translatable("skyblocker.exotic." + name).formatted(formatting);
+ }
+ }
+
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/DungeonQualityTooltip.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/DungeonQualityTooltip.java
new file mode 100644
index 00000000..0b1d993d
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/DungeonQualityTooltip.java
@@ -0,0 +1,59 @@
+package de.hysky.skyblocker.skyblock.item.tooltip.adders;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.skyblock.item.tooltip.TooltipAdder;
+import de.hysky.skyblocker.utils.ItemUtils;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.NbtCompound;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+public class DungeonQualityTooltip extends TooltipAdder {
+ public DungeonQualityTooltip(int priority) {
+ super(priority);
+ }
+
+ @Override
+ public void addToTooltip(@Nullable Slot focusedSlot, ItemStack stack, List<Text> lines) {
+ if (!SkyblockerConfigManager.get().general.itemTooltip.dungeonQuality) return;
+ NbtCompound customData = ItemUtils.getCustomData(stack);
+ if (customData == null || !customData.contains("baseStatBoostPercentage")) return;
+ int baseStatBoostPercentage = customData.getInt("baseStatBoostPercentage");
+ boolean maxQuality = baseStatBoostPercentage == 50;
+ if (maxQuality) {
+ lines.add(Text.literal(String.format("%-17s", "Item Quality:") + baseStatBoostPercentage + "/50").formatted(Formatting.RED).formatted(Formatting.BOLD));
+ } else {
+ lines.add(Text.literal(String.format("%-21s", "Item Quality:") + baseStatBoostPercentage + "/50").formatted(Formatting.BLUE));
+ }
+
+ if (customData.contains("item_tier")) { // sometimes it just isn't here?
+ int itemTier = customData.getInt("item_tier");
+ if (maxQuality) {
+ lines.add(Text.literal(String.format("%-17s", "Floor Tier:") + itemTier + " (" + getItemTierFloor(itemTier) + ")").formatted(Formatting.RED).formatted(Formatting.BOLD));
+ } else {
+ lines.add(Text.literal(String.format("%-21s", "Floor Tier:") + itemTier + " (" + getItemTierFloor(itemTier) + ")").formatted(Formatting.BLUE));
+ }
+ }
+ }
+
+ final String getItemTierFloor(int tier) {
+ return switch (tier) {
+ case 0 -> "E";
+ case 1 -> "F1";
+ case 2 -> "F2";
+ case 3 -> "F3";
+ case 4 -> "F4/M1";
+ case 5 -> "F5/M2";
+ case 6 -> "F6/M3";
+ case 7 -> "F7/M4";
+ case 8 -> "M5";
+ case 9 -> "M6";
+ case 10 -> "M7";
+ default -> "Unknown";
+ };
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/LBinTooltip.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/LBinTooltip.java
new file mode 100644
index 00000000..e6930c32
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/LBinTooltip.java
@@ -0,0 +1,40 @@
+package de.hysky.skyblocker.skyblock.item.tooltip.adders;
+
+import de.hysky.skyblocker.skyblock.item.tooltip.ItemTooltip;
+import de.hysky.skyblocker.skyblock.item.tooltip.TooltipAdder;
+import de.hysky.skyblocker.skyblock.item.tooltip.TooltipInfoType;
+import net.minecraft.item.ItemStack;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+public class LBinTooltip extends TooltipAdder {
+ public static boolean lbinExist = false;
+
+ public LBinTooltip(int priority) {
+ super(priority);
+ }
+
+ @Override
+ public void addToTooltip(@Nullable Slot focusedSlot, ItemStack stack, List<Text> lines) {
+ lbinExist = false;
+ final String internalID = stack.getSkyblockId();
+ if (internalID == null) return;
+ String name = stack.getSkyblockApiId();
+ if (name == null) return;
+
+ if (name.startsWith("ISSHINY_")) name = "SHINY_" + internalID;
+
+ // bazaarOpened & bazaarExist check for lbin, because Skytils keeps some bazaar item data in lbin api
+
+ if (TooltipInfoType.LOWEST_BINS.isTooltipEnabledAndHasOrNullWarning(name) && !BazaarPriceTooltip.bazaarExist) {
+ lines.add(Text.literal(String.format("%-19s", "Lowest BIN Price:"))
+ .formatted(Formatting.GOLD)
+ .append(ItemTooltip.getCoinsMessage(TooltipInfoType.LOWEST_BINS.getData().get(name).getAsDouble(), stack.getCount())));
+ lbinExist = true;
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/LineSmoothener.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/LineSmoothener.java
new file mode 100644
index 00000000..0e997834
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/LineSmoothener.java
@@ -0,0 +1,34 @@
+package de.hysky.skyblocker.skyblock.item.tooltip.adders;
+
+import de.hysky.skyblocker.skyblock.item.tooltip.TooltipAdder;
+import net.minecraft.item.ItemStack;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+public class LineSmoothener extends TooltipAdder {
+ //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);
+
+ public static Text createSmoothLine() {
+ return Text.literal(" ").formatted(Formatting.DARK_GRAY, Formatting.STRIKETHROUGH, Formatting.BOLD);
+ }
+
+ public LineSmoothener() {
+ super(Integer.MIN_VALUE);
+ }
+
+ @Override
+ public void addToTooltip(@Nullable Slot focusedSlot, ItemStack stack, List<Text> lines) {
+ for (int i = 0; i < lines.size(); i++) {
+ 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());
+ }
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/MotesTooltip.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/MotesTooltip.java
new file mode 100644
index 00000000..a0aa8d94
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/MotesTooltip.java
@@ -0,0 +1,50 @@
+package de.hysky.skyblocker.skyblock.item.tooltip.adders;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.skyblock.item.tooltip.TooltipAdder;
+import de.hysky.skyblocker.skyblock.item.tooltip.TooltipInfoType;
+import net.minecraft.item.ItemStack;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.MutableText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+import java.util.Locale;
+
+public class MotesTooltip extends TooltipAdder {
+ public MotesTooltip(int priority) {
+ super(priority);
+ }
+
+ @Override
+ public void addToTooltip(@Nullable Slot focusedSlot, ItemStack stack, List<Text> lines) {
+ final String internalID = stack.getSkyblockId();
+ if (internalID != null && TooltipInfoType.MOTES.isTooltipEnabledAndHasOrNullWarning(internalID)) {
+ lines.add(Text.literal(String.format("%-20s", "Motes Price:"))
+ .formatted(Formatting.LIGHT_PURPLE)
+ .append(getMotesMessage(TooltipInfoType.MOTES.getData().get(internalID).getAsInt(), stack.getCount())));
+ }
+ }
+
+ private static Text getMotesMessage(int price, int count) {
+ float motesMultiplier = SkyblockerConfigManager.get().otherLocations.rift.mcGrubberStacks * 0.05f + 1;
+
+ // Calculate the total price
+ int totalPrice = price * count;
+ String totalPriceString = String.format(Locale.ENGLISH, "%1$,.1f", totalPrice * motesMultiplier);
+
+ // If count is 1, return a simple message
+ if (count == 1) {
+ return Text.literal(totalPriceString.replace(".0", "") + " Motes").formatted(Formatting.DARK_AQUA);
+ }
+
+ // If count is greater than 1, include the "each" information
+ String eachPriceString = String.format(Locale.ENGLISH, "%1$,.1f", price * motesMultiplier);
+ MutableText message = Text.literal(totalPriceString.replace(".0", "") + " Motes ").formatted(Formatting.DARK_AQUA);
+ message.append(Text.literal("(" + eachPriceString.replace(".0", "") + " each)").formatted(Formatting.GRAY));
+
+ return message;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/MuseumTooltip.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/MuseumTooltip.java
new file mode 100644
index 00000000..5c21d2df
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/MuseumTooltip.java
@@ -0,0 +1,49 @@
+package de.hysky.skyblocker.skyblock.item.tooltip.adders;
+
+import de.hysky.skyblocker.skyblock.item.MuseumItemCache;
+import de.hysky.skyblocker.skyblock.item.tooltip.TooltipAdder;
+import de.hysky.skyblocker.skyblock.item.tooltip.TooltipInfoType;
+import de.hysky.skyblocker.utils.ItemUtils;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.NbtCompound;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+public class MuseumTooltip extends TooltipAdder {
+ public MuseumTooltip(int priority) {
+ super(priority);
+ }
+
+ @Override
+ public void addToTooltip(@Nullable Slot focusedSlot, ItemStack stack, List<Text> lines) {
+ final String internalID = stack.getSkyblockId();
+ if (TooltipInfoType.MUSEUM.isTooltipEnabledAndHasOrNullWarning(internalID)) {
+ String itemCategory = TooltipInfoType.MUSEUM.getData().get(internalID).getAsString();
+ String format = switch (itemCategory) {
+ case "Weapons" -> "%-18s";
+ case "Armor" -> "%-19s";
+ default -> "%-20s";
+ };
+
+ //Special case the special category so that it doesn't always display not donated
+ if (itemCategory.equals("Special")) {
+ lines.add(Text.literal(String.format(format, "Museum: (" + itemCategory + ")"))
+ .formatted(Formatting.LIGHT_PURPLE));
+ } else {
+ NbtCompound customData = ItemUtils.getCustomData(stack);
+ boolean isInMuseum = (customData.contains("donated_museum") && customData.getBoolean("donated_museum")) || MuseumItemCache.hasItemInMuseum(internalID);
+
+ Formatting donatedIndicatorFormatting = isInMuseum ? Formatting.GREEN : Formatting.RED;
+
+ lines.add(Text.literal(String.format(format, "Museum (" + itemCategory + "):"))
+ .formatted(Formatting.LIGHT_PURPLE)
+ .append(Text.literal(isInMuseum ? "✔" : "✖").formatted(donatedIndicatorFormatting, Formatting.BOLD))
+ .append(Text.literal(isInMuseum ? " Donated" : " Not Donated").formatted(donatedIndicatorFormatting)));
+ }
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/NpcPriceTooltip.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/NpcPriceTooltip.java
new file mode 100644
index 00000000..672201d5
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/NpcPriceTooltip.java
@@ -0,0 +1,28 @@
+package de.hysky.skyblocker.skyblock.item.tooltip.adders;
+
+import de.hysky.skyblocker.skyblock.item.tooltip.ItemTooltip;
+import de.hysky.skyblocker.skyblock.item.tooltip.TooltipAdder;
+import de.hysky.skyblocker.skyblock.item.tooltip.TooltipInfoType;
+import net.minecraft.item.ItemStack;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+public class NpcPriceTooltip extends TooltipAdder {
+ public NpcPriceTooltip(int priority) {
+ super(priority);
+ }
+
+ @Override
+ public void addToTooltip(@Nullable Slot focusedSlot, ItemStack stack, List<Text> lines) {
+ final String internalID = stack.getSkyblockId();
+ if (internalID != null && TooltipInfoType.NPC.isTooltipEnabledAndHasOrNullWarning(internalID)) {
+ lines.add(Text.literal(String.format("%-21s", "NPC Sell Price:"))
+ .formatted(Formatting.YELLOW)
+ .append(ItemTooltip.getCoinsMessage(TooltipInfoType.NPC.getData().get(internalID).getAsDouble(), stack.getCount())));
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/ObtainedDateTooltip.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/ObtainedDateTooltip.java
new file mode 100644
index 00000000..9f405c58
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/ObtainedDateTooltip.java
@@ -0,0 +1,73 @@
+package de.hysky.skyblocker.skyblock.item.tooltip.adders;
+
+import de.hysky.skyblocker.skyblock.item.tooltip.TooltipAdder;
+import de.hysky.skyblocker.skyblock.item.tooltip.TooltipInfoType;
+import de.hysky.skyblocker.utils.ItemUtils;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.NbtCompound;
+import net.minecraft.nbt.NbtElement;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import org.jetbrains.annotations.Nullable;
+
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.TemporalAccessor;
+import java.util.List;
+import java.util.Locale;
+
+public class ObtainedDateTooltip extends TooltipAdder {
+ private static final DateTimeFormatter OBTAINED_DATE_FORMATTER = DateTimeFormatter.ofPattern("MMMM d, yyyy").withZone(ZoneId.systemDefault()).localizedBy(Locale.ENGLISH);
+ private static final DateTimeFormatter OLD_OBTAINED_DATE_FORMAT = DateTimeFormatter.ofPattern("M/d/yy h:m a").withZone(ZoneId.of("UTC")).localizedBy(Locale.ENGLISH);
+
+ public ObtainedDateTooltip(int priority) {
+ super(priority);
+ }
+
+ @Override
+ public void addToTooltip(@Nullable Slot focusedSlot, ItemStack stack, List<Text> lines) {
+ if (TooltipInfoType.OBTAINED.isTooltipEnabled()) {
+ String timestamp = getTimestamp(stack);
+
+ if (!timestamp.isEmpty()) {
+ lines.add(Text.empty()
+ .append(Text.literal(String.format("%-21s", "Obtained: ")).formatted(Formatting.LIGHT_PURPLE))
+ .append(Text.literal(timestamp).formatted(Formatting.RED)));
+ }
+ }
+ }
+
+ /**
+ * This method converts the "timestamp" variable into the same date format as Hypixel represents it in the museum.
+ * Currently, there are two types of string timestamps the legacy which is built like this
+ * "dd/MM/yy hh:mm" ("25/04/20 16:38") and the current which is built like this
+ * "MM/dd/yy hh:mm aa" ("12/24/20 11:08 PM"). Since Hypixel transforms the two formats into one format without
+ * taking into account of their formats, we do the same. The final result looks like this
+ * "MMMM dd, yyyy" (December 24, 2020).
+ * Since the legacy format has a 25 as "month" SimpleDateFormat converts the 25 into 2 years and 1 month and makes
+ * "25/04/20 16:38" -> "January 04, 2022" instead of "April 25, 2020".
+ * This causes the museum rank to be much worse than it should be.
+ * <p>
+ * This also handles the long timestamp format introduced in January 2024 where the timestamp is in epoch milliseconds.
+ *
+ * @param stack the item under the pointer
+ * @return if the item have a "Timestamp" it will be shown formated on the tooltip
+ */
+ public static String getTimestamp(ItemStack stack) {
+ NbtCompound customData = ItemUtils.getCustomData(stack);
+
+ if (customData != null && customData.contains("timestamp", NbtElement.LONG_TYPE)) {
+ Instant date = Instant.ofEpochMilli(customData.getLong("timestamp"));
+ return OBTAINED_DATE_FORMATTER.format(date);
+ }
+
+ if (customData != null && customData.contains("timestamp", NbtElement.STRING_TYPE)) {
+ TemporalAccessor date = OLD_OBTAINED_DATE_FORMAT.parse(customData.getString("timestamp"));
+ return OBTAINED_DATE_FORMATTER.format(date);
+ }
+
+ return "";
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/SupercraftReminder.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/SupercraftReminder.java
new file mode 100644
index 00000000..47d2bd48
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/adders/SupercraftReminder.java
@@ -0,0 +1,32 @@
+package de.hysky.skyblocker.skyblock.item.tooltip.adders;
+
+import de.hysky.skyblocker.skyblock.item.tooltip.TooltipAdder;
+import de.hysky.skyblocker.utils.ItemUtils;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.Items;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+import java.util.regex.Pattern;
+
+public class SupercraftReminder extends TooltipAdder {
+ private static final byte SUPERCRAFT_SLOT = 32;
+ private static final byte RECIPE_RESULT_SLOT = 25;
+
+ public SupercraftReminder() {
+ super(Pattern.compile("^.+ Recipe$"), Integer.MIN_VALUE);
+ }
+
+ @Override
+ public void addToTooltip(@Nullable Slot focusedSlot, ItemStack stack, List<Text> lines) {
+ if (focusedSlot == null || focusedSlot.id != SUPERCRAFT_SLOT || !stack.isOf(Items.GOLDEN_PICKAXE)) return;
+ String uuid = ItemUtils.getItemUuid(focusedSlot.inventory.getStack(RECIPE_RESULT_SLOT));
+ if (!uuid.isEmpty()) return; //Items with UUID can't be stacked, and therefore the shift-click feature doesn't matter
+ int index = lines.size() - 1;
+ if (lines.get(lines.size() - 2).getString().equals("Recipe not unlocked!")) index--; //Place it right below the "Right-Click to set amount" line
+ lines.add(index, Text.literal("Shift-Click to maximize the amount!").formatted(Formatting.GOLD));
+ }
+}
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/ResultButtonWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ResultButtonWidget.java
index d2d463c7..6af03f31 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ResultButtonWidget.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ResultButtonWidget.java
@@ -1,30 +1,27 @@
package de.hysky.skyblocker.skyblock.itemlist;
-import java.util.List;
-import java.util.ArrayList;
-
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.screen.Screen;
import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder;
import net.minecraft.client.gui.widget.ClickableWidget;
import net.minecraft.item.ItemStack;
-import net.minecraft.item.Items;
-import net.minecraft.text.OrderedText;
import net.minecraft.text.Text;
import net.minecraft.util.Identifier;
+import java.util.List;
+
public class ResultButtonWidget extends ClickableWidget {
private static final Identifier BACKGROUND_TEXTURE = new Identifier("recipe_book/slot_craftable");
protected ItemStack itemStack = null;
public ResultButtonWidget(int x, int y) {
- super(x, y, 25, 25, Text.of(""));
+ super(x, y, 25, 25, Text.literal(""));
}
protected void setItemStack(ItemStack itemStack) {
- this.active = !itemStack.getItem().equals(Items.AIR);
+ this.active = !itemStack.isEmpty();
this.visible = true;
this.itemStack = itemStack;
}
@@ -37,29 +34,18 @@ public class ResultButtonWidget extends ClickableWidget {
@Override
public void renderWidget(DrawContext context, int mouseX, int mouseY, float delta) {
MinecraftClient client = MinecraftClient.getInstance();
- // this.drawTexture(matrices, this.x, this.y, 29, 206, this.width, this.height);
context.drawGuiTexture(BACKGROUND_TEXTURE, this.getX(), this.getY(), this.getWidth(), this.getHeight());
- // client.getItemRenderer().renderInGui(this.itemStack, this.x + 4, this.y + 4);
context.drawItem(this.itemStack, this.getX() + 4, this.getY() + 4);
- // client.getItemRenderer().renderGuiItemOverlay(client.textRenderer, itemStack, this.x + 4, this.y + 4);
context.drawItemInSlot(client.textRenderer, itemStack, this.getX() + 4, this.getY() + 4);
}
public void renderTooltip(DrawContext context, int mouseX, int mouseY) {
MinecraftClient client = MinecraftClient.getInstance();
+ if (client.currentScreen == null) return;
List<Text> tooltip = Screen.getTooltipFromItem(client, this.itemStack);
- List<OrderedText> orderedTooltip = new ArrayList<>();
-
- for(int i = 0; i < tooltip.size(); i++) {
- orderedTooltip.add(tooltip.get(i).asOrderedText());
- }
-
- client.currentScreen.setTooltip(orderedTooltip);
+ client.currentScreen.setTooltip(tooltip.stream().map(Text::asOrderedText).toList());
}
- @Override
- protected void appendClickableNarrations(NarrationMessageBuilder builder) {
- // TODO Auto-generated method stub
-
- }
+ @Override
+ protected void appendClickableNarrations(NarrationMessageBuilder builder) {}
}
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)),
diff --git a/src/main/java/de/hysky/skyblocker/utils/ItemUtils.java b/src/main/java/de/hysky/skyblocker/utils/ItemUtils.java
index 13b28808..46950fc4 100644
--- a/src/main/java/de/hysky/skyblocker/utils/ItemUtils.java
+++ b/src/main/java/de/hysky/skyblocker/utils/ItemUtils.java
@@ -1,5 +1,7 @@
package de.hysky.skyblocker.utils;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.mojang.authlib.properties.Property;
import com.mojang.authlib.properties.PropertyMap;
@@ -8,10 +10,15 @@ import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.serialization.Codec;
import com.mojang.serialization.JsonOps;
import com.mojang.serialization.codecs.RecordCodecBuilder;
+import de.hysky.skyblocker.skyblock.item.tooltip.adders.ObtainedDateTooltip;
import de.hysky.skyblocker.SkyblockerMod;
+import de.hysky.skyblocker.skyblock.item.tooltip.TooltipInfoType;
+import it.unimi.dsi.fastutil.doubles.DoubleBooleanPair;
import it.unimi.dsi.fastutil.ints.IntIntPair;
+import it.unimi.dsi.fastutil.longs.LongBooleanPair;
import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource;
import net.minecraft.component.ComponentChanges;
+import net.minecraft.component.ComponentHolder;
import net.minecraft.component.DataComponentTypes;
import net.minecraft.component.type.LoreComponent;
import net.minecraft.component.type.NbtComponent;
@@ -20,7 +27,6 @@ import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.item.Items;
import net.minecraft.nbt.NbtCompound;
-import net.minecraft.nbt.NbtElement;
import net.minecraft.registry.Registries;
import net.minecraft.registry.entry.RegistryEntry;
import net.minecraft.text.Text;
@@ -29,13 +35,7 @@ import net.minecraft.util.dynamic.Codecs;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
-import java.time.Instant;
-import java.time.ZoneId;
-import java.time.format.DateTimeFormatter;
-import java.time.temporal.TemporalAccessor;
-import java.util.Iterator;
import java.util.List;
-import java.util.Locale;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.regex.Matcher;
@@ -46,8 +46,6 @@ import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.lit
public class ItemUtils {
public static final String ID = "id";
public static final String UUID = "uuid";
- private static final DateTimeFormatter OBTAINED_DATE_FORMATTER = DateTimeFormatter.ofPattern("MMMM d, yyyy").withZone(ZoneId.systemDefault()).localizedBy(Locale.ENGLISH);
- private static final DateTimeFormatter OLD_OBTAINED_DATE_FORMAT = DateTimeFormatter.ofPattern("M/d/yy h:m a").withZone(ZoneId.of("UTC")).localizedBy(Locale.ENGLISH);
public static final Pattern NOT_DURABILITY = Pattern.compile("[^0-9 /]");
public static final Predicate<String> FUEL_PREDICATE = line -> line.contains("Fuel: ");
private static final Codec<RegistryEntry<Item>> EMPTY_ALLOWING_ITEM_CODEC = Registries.ITEM.getEntryCodec();
@@ -65,7 +63,7 @@ public class ItemUtils {
}
@SuppressWarnings("deprecation")
- public static NbtCompound getCustomData(@NotNull ItemStack stack) {
+ public static NbtCompound getCustomData(@NotNull ComponentHolder stack) {
return stack.getOrDefault(DataComponentTypes.CUSTOM_DATA, NbtComponent.DEFAULT).getNbt();
}
@@ -75,7 +73,7 @@ public class ItemUtils {
* @param stack the item stack to get the internal name from
* @return an optional containing the internal name of the item stack
*/
- public static Optional<String> getItemIdOptional(@NotNull ItemStack stack) {
+ public static Optional<String> getItemIdOptional(@NotNull ItemStack stack) {
NbtCompound customData = getCustomData(stack);
return customData.contains(ID) ? Optional.of(customData.getString(ID)) : Optional.empty();
}
@@ -86,7 +84,7 @@ public class ItemUtils {
* @param stack the item stack to get the internal name from
* @return the internal name of the item stack, or an empty string if the item stack is null or does not have an internal name
*/
- public static String getItemId(@NotNull ItemStack stack) {
+ public static String getItemId(@NotNull ItemStack stack) {
return getCustomData(stack).getString(ID);
}
@@ -96,7 +94,7 @@ public class ItemUtils {
* @param stack the item stack to get the UUID from
* @return an optional containing the UUID of the item stack
*/
- public static Optional<String> getItemUuidOptional(@NotNull ItemStack stack) {
+ public static Optional<String> getItemUuidOptional(@NotNull ItemStack stack) {
NbtCompound customData = getCustomData(stack);
return customData.contains(UUID) ? Optional.of(customData.getString(UUID)) : Optional.empty();
}
@@ -107,11 +105,46 @@ public class ItemUtils {
* @param stack the item stack to get the UUID from
* @return the UUID of the item stack, or an empty string if the item stack is null or does not have a UUID
*/
- public static String getItemUuid(@NotNull ItemStack stack) {
+ public static String getItemUuid(@NotNull ComponentHolder stack) {
return getCustomData(stack).getString(UUID);
}
/**
+ * Gets the bazaar sell price or the lowest bin based on the id of the item stack.
+ *
+ * @return An {@link LongBooleanPair} with the {@code left long} representing the item's price,
+ * and the {@code right boolean} indicating if the price was based on complete data.
+ */
+ public static DoubleBooleanPair getItemPrice(@NotNull ItemStack stack) {
+ return getItemPrice(getItemId(stack));
+ }
+
+ /**
+ * Gets the bazaar sell price or the lowest bin of the item with the specified id.
+ *
+ * @return An {@link LongBooleanPair} with the {@code left long} representing the item's price,
+ * and the {@code right boolean} indicating if the price was based on complete data.
+ */
+ public static DoubleBooleanPair getItemPrice(@Nullable String id) {
+ JsonObject bazaarPrices = TooltipInfoType.BAZAAR.getData();
+ JsonObject lowestBinPrices = TooltipInfoType.LOWEST_BINS.getData();
+
+ if (id == null || id.isEmpty() || bazaarPrices == null || lowestBinPrices == null) return DoubleBooleanPair.of(0, false);
+
+ if (bazaarPrices.has(id)) {
+ JsonElement sellPrice = bazaarPrices.get(id).getAsJsonObject().get("sellPrice");
+ boolean isPriceNull = sellPrice.isJsonNull();
+ return DoubleBooleanPair.of(isPriceNull ? 0 : sellPrice.getAsDouble(), !isPriceNull);
+ }
+
+ if (lowestBinPrices.has(id)) {
+ return DoubleBooleanPair.of(lowestBinPrices.get(id).getAsDouble(), true);
+ }
+
+ return DoubleBooleanPair.of(0, false);
+ }
+
+ /**
* This method converts the "timestamp" variable into the same date format as Hypixel represents it in the museum.
* Currently, there are two types of string timestamps the legacy which is built like this
* "dd/MM/yy hh:mm" ("25/04/20 16:38") and the current which is built like this
@@ -126,21 +159,12 @@ public class ItemUtils {
*
* @param stack the item under the pointer
* @return if the item have a "Timestamp" it will be shown formated on the tooltip
+ * @deprecated use {@link ObtainedDateTooltip#getTimestamp(ItemStack)} instead
*/
- public static String getTimestamp(ItemStack stack) {
+ public static String getTimestamp(ItemStack stack) {
NbtCompound customData = getCustomData(stack);
- if (customData != null && customData.contains("timestamp", NbtElement.LONG_TYPE)) {
- Instant date = Instant.ofEpochMilli(customData.getLong("timestamp"));
- return OBTAINED_DATE_FORMATTER.format(date);
- }
-
- if (customData != null && customData.contains("timestamp", NbtElement.STRING_TYPE)) {
- TemporalAccessor date = OLD_OBTAINED_DATE_FORMAT.parse(customData.getString("timestamp"));
- return OBTAINED_DATE_FORMATTER.format(date);
- }
-
- return "";
+ return ObtainedDateTooltip.getTimestamp(stack);
}
public static boolean hasCustomDurability(@NotNull ItemStack stack) {
@@ -198,7 +222,7 @@ public class ItemUtils {
public static List<Text> getLore(ItemStack item) {
return item.getOrDefault(DataComponentTypes.LORE, LoreComponent.DEFAULT).styledLines();
}
-
+
public static PropertyMap propertyMapWithTexture(String textureValue) {
return Codecs.GAME_PROFILE_PROPERTY_MAP.parse(JsonOps.INSTANCE, JsonParser.parseString("[{\"name\":\"textures\",\"value\":\"" + textureValue + "\"}]")).getOrThrow();
}
@@ -207,12 +231,12 @@ public class ItemUtils {
if (!stack.isOf(Items.PLAYER_HEAD) || !stack.contains(DataComponentTypes.PROFILE)) return "";
ProfileComponent profile = stack.get(DataComponentTypes.PROFILE);
- String texture = profile.properties().get("textures").stream()
+ if (profile == null) return "";
+
+ return profile.properties().get("textures").stream()
.map(Property::value)
.findFirst()
.orElse("");
-
- return texture;
}
public static Optional<String> getHeadTextureOptional(ItemStack stack) {
diff --git a/src/main/java/de/hysky/skyblocker/utils/RomanNumerals.java b/src/main/java/de/hysky/skyblocker/utils/RomanNumerals.java
new file mode 100644
index 00000000..007cb0b1
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/utils/RomanNumerals.java
@@ -0,0 +1,54 @@
+package de.hysky.skyblocker.utils;
+
+public class RomanNumerals {
+ private RomanNumerals() {
+ }
+ private static int getDecimalValue(char romanChar) {
+ return switch (romanChar) {
+ case 'I' -> 1;
+ case 'V' -> 5;
+ case 'X' -> 10;
+ case 'L' -> 50;
+ case 'C' -> 100;
+ case 'D' -> 500;
+ case 'M' -> 1000;
+ default -> 0;
+ };
+ }
+
+ /**
+ * Checks if a string is a valid roman numeral.
+ * It's the caller's responsibility to clean up the string before calling this method (such as trimming it).
+ * @param romanNumeral The roman numeral to check.
+ * @return True if the string is a valid roman numeral, false otherwise.
+ * @implNote This will only check if the string contains valid roman numeral characters. It won't check if the numeral is well-formed.
+ */
+ public static boolean isValidRomanNumeral(String romanNumeral) {
+ if (romanNumeral == null || romanNumeral.isEmpty()) return false;
+ for (int i = 0; i < romanNumeral.length(); i++) {
+ if (getDecimalValue(romanNumeral.charAt(i)) == 0) return false;
+ }
+ return true;
+ }
+
+ /**
+ * Converts a roman numeral to a decimal number.
+ *
+ * @param romanNumeral The roman numeral to convert.
+ * @return The decimal number, or 0 if the roman numeral string has non-roman characters in it, or if the string is empty or null.
+ */
+ public static int romanToDecimal(String romanNumeral) {
+ if (romanNumeral == null || romanNumeral.isEmpty()) return 0;
+ romanNumeral = romanNumeral.trim().toUpperCase();
+ int decimal = 0;
+ int lastNumber = 0;
+ for (int i = romanNumeral.length() - 1; i >= 0; i--) {
+ char ch = romanNumeral.charAt(i);
+ int number = getDecimalValue(ch);
+ if (number == 0) return 0; //Malformed roman numeral
+ decimal = number >= lastNumber ? decimal + number : decimal - number;
+ lastNumber = number;
+ }
+ return decimal;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/utils/Utils.java b/src/main/java/de/hysky/skyblocker/utils/Utils.java
index 62a3b897..925879b8 100644
--- a/src/main/java/de/hysky/skyblocker/utils/Utils.java
+++ b/src/main/java/de/hysky/skyblocker/utils/Utils.java
@@ -5,11 +5,9 @@ import com.google.gson.JsonParser;
import de.hysky.skyblocker.events.SkyblockEvents;
import de.hysky.skyblocker.mixins.accessors.MessageHandlerAccessor;
import de.hysky.skyblocker.skyblock.item.MuseumItemCache;
-import de.hysky.skyblocker.skyblock.item.tooltip.ItemTooltip;
import de.hysky.skyblocker.utils.scheduler.MessageScheduler;
import de.hysky.skyblocker.utils.scheduler.Scheduler;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
-import net.fabricmc.fabric.api.client.item.v1.ItemTooltipCallback;
import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents;
import net.fabricmc.fabric.api.networking.v1.PacketSender;
@@ -19,6 +17,7 @@ import net.minecraft.client.network.ClientPlayNetworkHandler;
import net.minecraft.client.network.ClientPlayerEntity;
import net.minecraft.client.network.PlayerListEntry;
import net.minecraft.scoreboard.*;
+import net.minecraft.text.MutableText;
import net.minecraft.text.Text;
import net.minecraft.util.Formatting;
import org.jetbrains.annotations.NotNull;
@@ -234,7 +233,6 @@ public class Utils {
if (!isOnSkyblock) {
if (!isInjected) {
isInjected = true;
- ItemTooltipCallback.EVENT.register(ItemTooltip::getTooltip);
}
isOnSkyblock = true;
SkyblockEvents.JOIN.invoker().onSkyblockJoin();
@@ -355,6 +353,23 @@ public class Utils {
}
}
+ // TODO: Combine with `ChocolateFactorySolver.formatTime` and move into `SkyblockTime`.
+ public static Text getDurationText(int timeInSeconds) {
+ int seconds = timeInSeconds % 60;
+ int minutes = (timeInSeconds/60) % 60;
+ int hours = (timeInSeconds/3600);
+
+ MutableText time = Text.empty();
+ if (hours > 0) {
+ time.append(hours + "h").append(" ");
+ }
+ if (hours > 0 || minutes > 0) {
+ time.append(minutes + "m").append(" ");
+ }
+ time.append(seconds + "s");
+ return time;
+ }
+
private static void updateFromPlayerList(MinecraftClient client) {
if (client.getNetworkHandler() == null) {
return;
diff --git a/src/main/java/de/hysky/skyblocker/utils/config/DurationController.java b/src/main/java/de/hysky/skyblocker/utils/config/DurationController.java
new file mode 100644
index 00000000..09edcf3c
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/utils/config/DurationController.java
@@ -0,0 +1,70 @@
+package de.hysky.skyblocker.utils.config;
+
+import de.hysky.skyblocker.utils.Utils;
+import dev.isxander.yacl3.api.Option;
+import dev.isxander.yacl3.api.utils.Dimension;
+import dev.isxander.yacl3.gui.AbstractWidget;
+import dev.isxander.yacl3.gui.YACLScreen;
+import dev.isxander.yacl3.gui.controllers.string.IStringController;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public record DurationController(Option<Integer> option) implements IStringController<Integer> {
+
+ private static final Pattern secondsPattern = Pattern.compile("(^|\\s)(\\d+)s(\\s|$)");
+ private static final Pattern minutesPattern = Pattern.compile("(^|\\s)(\\d+)m(\\s|$)");
+ private static final Pattern hoursPattern = Pattern.compile("(^|\\s)(\\d+)h(\\s|$)");
+
+ @Override
+ public String getString() {
+ return Utils.getDurationText(option.pendingValue()).getString();
+ }
+
+
+ @Override
+ public void setFromString(String value) {
+ Matcher hoursMatcher = hoursPattern.matcher(value);
+ Matcher minutesMatcher = minutesPattern.matcher(value);
+ Matcher secondsMatcher = secondsPattern.matcher(value);
+
+ int result = 0;
+ if (hoursMatcher.find()) {
+ result += Integer.parseInt(hoursMatcher.group(2)) * 3600;
+ }
+ if (minutesMatcher.find()) {
+ result += Integer.parseInt(minutesMatcher.group(2)) * 60;
+ }
+ if (secondsMatcher.find()) {
+ result += Integer.parseInt(secondsMatcher.group(2));
+ }
+ option.requestSet(result);
+ }
+
+
+ @Override
+ public boolean isInputValid(String s) {
+ Matcher hoursMatcher = hoursPattern.matcher(s);
+ Matcher minutesMatcher = minutesPattern.matcher(s);
+ Matcher secondsMatcher = secondsPattern.matcher(s);
+
+ int hoursCount = 0;
+ while (hoursMatcher.find()) hoursCount++;
+ int minutesCount = 0;
+ while (minutesMatcher.find()) minutesCount++;
+ int secondsCount = 0;
+ while (secondsMatcher.find()) secondsCount++;
+
+ if (hoursCount == 0 && minutesCount == 0 && secondsCount == 0) return false;
+ if (hoursCount > 1 || minutesCount > 1 || secondsCount > 1) return false;
+ s = s.replaceAll(hoursPattern.pattern(), "");
+ s = s.replaceAll(minutesPattern.pattern(), "");
+ s = s.replaceAll(secondsPattern.pattern(), "");
+ return s.isBlank();
+ }
+
+ @Override
+ public AbstractWidget provideWidget(YACLScreen screen, Dimension<Integer> widgetDimension) {
+ return new DurationControllerWidget(this, screen, widgetDimension);
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/utils/config/DurationControllerWidget.java b/src/main/java/de/hysky/skyblocker/utils/config/DurationControllerWidget.java
new file mode 100644
index 00000000..f25cd088
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/utils/config/DurationControllerWidget.java
@@ -0,0 +1,38 @@
+package de.hysky.skyblocker.utils.config;
+
+import dev.isxander.yacl3.api.utils.Dimension;
+import dev.isxander.yacl3.gui.YACLScreen;
+import dev.isxander.yacl3.gui.controllers.string.IStringController;
+import dev.isxander.yacl3.gui.controllers.string.StringControllerElement;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+import java.util.function.Consumer;
+
+public class DurationControllerWidget extends StringControllerElement {
+
+ public DurationControllerWidget(IStringController<?> control, YACLScreen screen, Dimension<Integer> dim) {
+ super(control, screen, dim, false);
+ }
+
+ @Override
+ public void unfocus() {
+ if (control.isInputValid(inputField)) super.unfocus();
+ else modifyInput(stringBuilder -> stringBuilder.replace(0, stringBuilder.length(), control.getString()));
+ }
+
+ @Override
+ public boolean modifyInput(Consumer<StringBuilder> consumer) {
+ StringBuilder temp = new StringBuilder(inputField);
+ consumer.accept(temp);
+ inputField = temp.toString();
+ return true;
+ }
+
+ @Override
+ protected Text getValueText() {
+ Text valueText = super.getValueText();
+ boolean inputValid = control.isInputValid(valueText.getString());
+ return valueText.copy().formatted(inputValid ? Formatting.WHITE: Formatting.RED);
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/utils/render/gui/AbstractContainerMatcher.java b/src/main/java/de/hysky/skyblocker/utils/render/gui/AbstractContainerMatcher.java
new file mode 100644
index 00000000..bf255218
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/utils/render/gui/AbstractContainerMatcher.java
@@ -0,0 +1,29 @@
+package de.hysky.skyblocker.utils.render.gui;
+
+import de.hysky.skyblocker.skyblock.ChestValue;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.regex.Pattern;
+
+public abstract class AbstractContainerMatcher {
+ /**
+ * The title of the screen must match this pattern for this to be applied. Null means it will be applied to all screens.
+ * @implNote Don't end your regex with a {@code $} as {@link ChestValue} appends text to the end of the title,
+ * so the regex will stop matching if the player uses chest value.
+ */
+ @Nullable
+ public final Pattern titlePattern;
+
+ protected AbstractContainerMatcher() {
+ this((Pattern) null);
+ }
+
+ protected AbstractContainerMatcher(@NotNull String titlePattern) {
+ this(Pattern.compile(titlePattern));
+ }
+
+ protected AbstractContainerMatcher(@Nullable Pattern titlePattern) {
+ this.titlePattern = titlePattern;
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/utils/render/gui/ContainerSolver.java b/src/main/java/de/hysky/skyblocker/utils/render/gui/ContainerSolver.java
index 0417dc3c..81c9ebec 100644
--- a/src/main/java/de/hysky/skyblocker/utils/render/gui/ContainerSolver.java
+++ b/src/main/java/de/hysky/skyblocker/utils/render/gui/ContainerSolver.java
@@ -11,17 +11,15 @@ import java.util.regex.Pattern;
/**
* Abstract class for gui solvers. Extend this class to add a new gui solver, like terminal solvers or experiment solvers.
*/
-public abstract class ContainerSolver {
- private final Pattern containerName;
-
- protected ContainerSolver(String containerName) {
- this.containerName = Pattern.compile(containerName);
+public abstract class ContainerSolver extends AbstractContainerMatcher {
+ protected ContainerSolver(String titlePattern) {
+ super(titlePattern);
}
protected abstract boolean isEnabled();
public final Pattern getName() {
- return containerName;
+ return titlePattern;
}
protected void start(GenericContainerScreen screen) {
diff --git a/src/main/java/de/hysky/skyblocker/utils/render/gui/SideTabButtonWidget.java b/src/main/java/de/hysky/skyblocker/utils/render/gui/SideTabButtonWidget.java
new file mode 100644
index 00000000..87da0d36
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/utils/render/gui/SideTabButtonWidget.java
@@ -0,0 +1,39 @@
+package de.hysky.skyblocker.utils.render.gui;
+
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.screen.ButtonTextures;
+import net.minecraft.client.gui.widget.ToggleButtonWidget;
+import net.minecraft.item.ItemStack;
+import net.minecraft.util.Identifier;
+import org.jetbrains.annotations.NotNull;
+
+public class SideTabButtonWidget extends ToggleButtonWidget {
+ private static final ButtonTextures TEXTURES = new ButtonTextures(new Identifier("recipe_book/tab"), new Identifier("recipe_book/tab_selected"));
+ protected @NotNull ItemStack icon;
+
+ public void setIcon(@NotNull ItemStack icon) {
+ this.icon = icon.copy();
+ }
+
+ public SideTabButtonWidget(int x, int y, boolean toggled, @NotNull ItemStack icon) {
+ super(x, y, 35, 27, toggled);
+ this.icon = icon.copy();
+ 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);
+ }
+
+ @Override
+ public void onClick(double mouseX, double mouseY) {
+ super.onClick(mouseX, mouseY);
+ if (!isToggled()) this.setToggled(true);
+ }
+}