aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/main/java/de/hysky/skyblocker/SkyblockerMod.java2
-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/configs/EventNotificationsConfig.java35
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/auction/widgets/CategoryTabWidget.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/itemlist/ItemListTab.java89
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemListWidget.java136
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/itemlist/SearchResultsWidget.java15
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/itemlist/UpcomingEventsTab.java168
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/JacobsContestWidget.java2
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/Utils.java18
-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/SideTabButtonWidget.java39
-rw-r--r--src/main/resources/assets/skyblocker/lang/en_us.json16
-rw-r--r--src/main/resources/assets/skyblocker/textures/gui/sprites/notification.pngbin0 -> 157 bytes
-rw-r--r--src/main/resources/assets/skyblocker/textures/gui/sprites/notification.png.mcmeta10
21 files changed, 989 insertions, 76 deletions
diff --git a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java
index a9aab3b6..19eb395a 100644
--- a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java
+++ b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java
@@ -26,6 +26,7 @@ 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;
@@ -177,6 +178,7 @@ public class SkyblockerMod implements ClientModInitializer {
Kuudra.init();
RenderHelper.init();
FancyStatusBars.init();
+ EventNotifications.init();
containerSolverManager.init();
statusBarTracker.init();
BeaconHighlighter.init();
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/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/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/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/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.