diff options
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.EnvType; import net.fabricmc.api.Environment; import net.minecraft.client.MinecraftClient; import net.minecraft.client.gui.DrawContext; import net.minecraft.client.gui.screen.recipebook.RecipeBookWidget; +import net.minecraft.client.gui.tooltip.Tooltip; +import net.minecraft.client.gui.widget.ContainerWidget; import net.minecraft.client.gui.widget.TextFieldWidget; -import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; import net.minecraft.screen.AbstractRecipeScreenHandler; import net.minecraft.text.Text; -import net.minecraft.util.Formatting; + +import java.util.ArrayList; +import java.util.List; @Environment(value = EnvType.CLIENT) public class ItemListWidget extends RecipeBookWidget { private int parentWidth; private int parentHeight; private int leftOffset; - private TextFieldWidget searchField; - private SearchResultsWidget results; + + private TabContainerWidget currentTabContent; + private final List<Pair<SideTabButtonWidget, TabContainerWidget>> tabs = new ArrayList<>(2); + private ItemListTab itemListTab; + + private static int currentTab = 0; public ItemListWidget() { super(); } - public void updateSearchResult() { - this.results.updateSearchResult(((RecipeBookWidgetAccessor) this).getSearchText()); - } - @Override public void initialize(int parentWidth, int parentHeight, MinecraftClient client, boolean narrow, AbstractRecipeScreenHandler<?> craftingScreenHandler) { super.initialize(parentWidth, parentHeight, client, narrow, craftingScreenHandler); this.parentWidth = parentWidth; this.parentHeight = parentHeight; this.leftOffset = narrow ? 0 : 86; - this.searchField = ((RecipeBookWidgetAccessor) this).getSearchField(); - int x = (this.parentWidth - 147) / 2 - this.leftOffset; - int y = (this.parentHeight - 166) / 2; - if (ItemRepository.filesImported()) { - this.results = new SearchResultsWidget(this.client, x, y); - this.updateSearchResult(); - } + TextFieldWidget searchField = ((RecipeBookWidgetAccessor) this).getSearchField(); + int x = (parentWidth - 147) / 2 - leftOffset; + int y = (parentHeight - 166) / 2; + + // Init all the tabs, content and the tab button on the left + tabs.clear(); + + // Item List + itemListTab = new ItemListTab(x + 9, y + 9, this.client, searchField); + SideTabButtonWidget itemListTabButton = new SideTabButtonWidget(x - 30, y + 3, currentTab == 0, new ItemStack(Items.CRAFTING_TABLE)); + itemListTabButton.setTooltip(Tooltip.of(Text.literal("Item List"))); + if (currentTab == 0) currentTabContent = itemListTab; + tabs.add(new ObjectObjectImmutablePair<>( + itemListTabButton, + this.itemListTab)); + + // Upcoming Events + UpcomingEventsTab upcomingEventsTab = new UpcomingEventsTab(x + 9, y + 9, this.client); + SideTabButtonWidget eventsTabButtonWidget = new SideTabButtonWidget(x - 30, y + 3 + 27, currentTab == 1, new ItemStack(Items.CLOCK)); + eventsTabButtonWidget.setTooltip(Tooltip.of(Text.literal("Upcoming Events"))); + if (currentTab == 1) currentTabContent = upcomingEventsTab; + tabs.add(new ObjectObjectImmutablePair<>( + eventsTabButtonWidget, + upcomingEventsTab + )); + + } + + @Override + public void reset() { + super.reset(); + if (itemListTab != null) itemListTab.setSearchField(((RecipeBookWidgetAccessor) this).getSearchField()); } @Override public void render(DrawContext context, int mouseX, int mouseY, float delta) { if (this.isOpen()) { - MatrixStack matrices = context.getMatrices(); - matrices.push(); - matrices.translate(0.0D, 0.0D, 100.0D); - RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F); - this.searchField = ((RecipeBookWidgetAccessor) this).getSearchField(); int i = (this.parentWidth - 147) / 2 - this.leftOffset; int j = (this.parentHeight - 166) / 2; + // Draw the texture context.drawTexture(TEXTURE, i, j, 1, 1, 147, 166); - this.searchField = ((RecipeBookWidgetAccessor) this).getSearchField(); - - if (!ItemRepository.filesImported() && !this.searchField.isFocused() && this.searchField.getText().isEmpty()) { - Text hintText = (Text.literal("Loading...")).formatted(Formatting.ITALIC).formatted(Formatting.GRAY); - context.drawTextWithShadow(this.client.textRenderer, hintText, i + 25, j + 14, -1); - } else if (!this.searchField.isFocused() && this.searchField.getText().isEmpty()) { - Text hintText = (Text.translatable("gui.recipebook.search_hint")).formatted(Formatting.ITALIC).formatted(Formatting.GRAY); - context.drawTextWithShadow(this.client.textRenderer, hintText, i + 25, j + 14, -1); - } else { - this.searchField.render(context, mouseX, mouseY, delta); - } - if (ItemRepository.filesImported()) { - if (results == null) { - int x = (this.parentWidth - 147) / 2 - this.leftOffset; - int y = (this.parentHeight - 166) / 2; - this.results = new SearchResultsWidget(this.client, x, y); - } - this.updateSearchResult(); - this.results.render(context, mouseX, mouseY, delta); + // Draw the tab's content + if (currentTabContent != null) currentTabContent.render(context, mouseX, mouseY, delta); + // Draw the tab buttons + for (Pair<SideTabButtonWidget, TabContainerWidget> tab : tabs) { + tab.left().render(context, mouseX, mouseY, delta); } - matrices.pop(); + } } @Override public void drawTooltip(DrawContext context, int x, int y, int mouseX, int mouseY) { - if (this.isOpen() && ItemRepository.filesImported() && results != null) { - this.results.drawTooltip(context, mouseX, mouseY); + if (this.isOpen() && currentTabContent != null) { + this.currentTabContent.drawTooltip(context, mouseX, mouseY); } } @Override public boolean mouseClicked(double mouseX, double mouseY, int button) { - if (this.isOpen() && this.client.player != null && !this.client.player.isSpectator() && ItemRepository.filesImported() && this.searchField != null && results != null) { - if (this.searchField.mouseClicked(mouseX, mouseY, button)) { - this.results.closeRecipeView(); - this.searchField.setFocused(true); - return true; - } else { - this.searchField.setFocused(false); - return this.results.mouseClicked(mouseX, mouseY, button); + if (this.isOpen() && this.client.player != null && !this.client.player.isSpectator()) { + // check if a tab is clicked + for (Pair<SideTabButtonWidget, TabContainerWidget> tab : tabs) { + if (tab.first().mouseClicked(mouseX, mouseY, button) && currentTabContent != tab.right()) { + for (Pair<SideTabButtonWidget, TabContainerWidget> tab2 : tabs) { + tab2.first().setToggled(false); + } + tab.first().setToggled(true); + currentTabContent = tab.right(); + currentTab = tabs.indexOf(tab); + return true; + } } + // click the tab content + if (currentTabContent != null) return currentTabContent.mouseClicked(mouseX, mouseY, button); + else return false; } else return false; } + + /** + * A container widget but with a fixed width and height and a drawTooltip method to implement + */ + public abstract static class TabContainerWidget extends ContainerWidget { + + public TabContainerWidget(int x, int y, Text text) { + super(x, y, 131, 150, text); + } + + public abstract void drawTooltip(DrawContext context, int mouseX, int mouseY); + } }
\ No newline at end of file diff --git a/src/main/java/de/hysky/skyblocker/skyblock/itemlist/SearchResultsWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/SearchResultsWidget.java index 1ef352e3..48d3a8f6 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/itemlist/SearchResultsWidget.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/SearchResultsWidget.java @@ -6,6 +6,7 @@ import net.minecraft.client.MinecraftClient; import net.minecraft.client.font.TextRenderer; import net.minecraft.client.gui.DrawContext; import net.minecraft.client.gui.Drawable; +import net.minecraft.client.gui.Element; import net.minecraft.client.gui.screen.ButtonTextures; import net.minecraft.client.gui.widget.ToggleButtonWidget; import net.minecraft.component.DataComponentTypes; @@ -23,7 +24,7 @@ import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; -public class SearchResultsWidget implements Drawable { +public class SearchResultsWidget implements Drawable, Element { private static final ButtonTextures PAGE_FORWARD_TEXTURES = new ButtonTextures(new Identifier("recipe_book/page_forward"), new Identifier("recipe_book/page_forward_highlighted")); private static final ButtonTextures PAGE_BACKWARD_TEXTURES = new ButtonTextures(new Identifier("recipe_book/page_backward"), new Identifier("recipe_book/page_backward_highlighted")); private static final int COLS = 5; @@ -225,4 +226,16 @@ public class SearchResultsWidget implements Drawable { return false; } + private boolean focused = false; + + @Override + public void setFocused(boolean focused) { + this.focused = focused; + } + + @Override + public boolean isFocused() { + return focused; + } + } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/itemlist/UpcomingEventsTab.java b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/UpcomingEventsTab.java new file mode 100644 index 00000000..9552ae87 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/UpcomingEventsTab.java @@ -0,0 +1,168 @@ +package de.hysky.skyblocker.skyblock.itemlist; + +import de.hysky.skyblocker.mixins.accessors.DrawContextInvoker; +import de.hysky.skyblocker.skyblock.events.EventNotifications; +import de.hysky.skyblocker.skyblock.tabhud.widget.JacobsContestWidget; +import de.hysky.skyblocker.utils.Utils; +import de.hysky.skyblocker.utils.scheduler.MessageScheduler; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.Element; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.gui.tooltip.HoveredTooltipPositioner; +import net.minecraft.client.gui.tooltip.TooltipComponent; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.text.MutableText; +import net.minecraft.text.Style; +import net.minecraft.text.Text; +import net.minecraft.util.Colors; +import net.minecraft.util.Formatting; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedList; +import java.util.List; + +public class UpcomingEventsTab extends ItemListWidget.TabContainerWidget { + private static final ItemStack CLOCK = new ItemStack(Items.CLOCK); + private final MinecraftClient client; + private final List<EventRenderer> events; + + public UpcomingEventsTab(int x, int y, MinecraftClient client) { + super(x, y, Text.literal("Upcoming Events Tab")); + this.client = client; + events = EventNotifications.getEvents().entrySet() + .stream() + .sorted(Comparator.comparingLong(a -> a.getValue().isEmpty() ? Long.MAX_VALUE : a.getValue().peekFirst().start())) + .map(stringLinkedListEntry -> new EventRenderer(stringLinkedListEntry.getKey(), stringLinkedListEntry.getValue())) + .toList(); + } + + @Override + public void drawTooltip(DrawContext context, int mouseX, int mouseY) { + if (hovered != null) { + ((DrawContextInvoker) context).invokeDrawTooltip(this.client.textRenderer, hovered.getTooltip(), mouseX, mouseY, HoveredTooltipPositioner.INSTANCE); + } + } + + @Override + public List<? extends Element> children() { + return List.of(); + } + + private EventRenderer hovered = null; + + @Override + protected void renderWidget(DrawContext context, int mouseX, int mouseY, float delta) { + int x = getX(); + int y = getY(); + context.enableScissor(x, y, getRight(), getBottom()); + context.drawItem(CLOCK, x, y + 4); + context.drawText(this.client.textRenderer, "Upcoming Events", x + 17, y + 7, -1, true); + + int eventsY = y + 7 + 24; + hovered = null; + for (EventRenderer eventRenderer : events) { + eventRenderer.render(context, x + 1, eventsY, mouseX, mouseY); + if (isMouseOver(mouseX, mouseY) && eventRenderer.isMouseOver(mouseX, mouseY, x+1, eventsY)) hovered = eventRenderer; + eventsY += eventRenderer.getHeight(); + + } + context.disableScissor(); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (hovered != null && hovered.getWarpCommand() != null) { + MessageScheduler.INSTANCE.sendMessageAfterCooldown(hovered.getWarpCommand()); + return true; + } + return false; + } + + @Override + protected void appendClickableNarrations(NarrationMessageBuilder builder) { + + } + + public static class EventRenderer { + + private final LinkedList<EventNotifications.SkyblockEvent> events; + private final String eventName; + + public EventRenderer(String eventName, LinkedList<EventNotifications.SkyblockEvent> events) { + this.events = events; + this.eventName = eventName; + } + + public void render(DrawContext context, int x, int y, int mouseX, int mouseY) { + long time = System.currentTimeMillis() / 1000; + TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer; + context.drawText(textRenderer, Text.literal(eventName).fillStyle(Style.EMPTY.withUnderline(isMouseOver(mouseX, mouseY, x, y))), x, y, -1, true); + if (events.isEmpty()) { + context.drawText(textRenderer, Text.literal(" ").append(Text.translatable("skyblocker.events.tab.noMore")), x, y + textRenderer.fontHeight, Colors.GRAY, false); + } else if (events.peekFirst().start() > time) { + MutableText formatted = Text.literal(" ").append(Text.translatable("skyblocker.events.tab.startsIn", Utils.getDurationText((int) (events.peekFirst().start() - time)))).formatted(Formatting.YELLOW); + context.drawText(textRenderer, formatted, x, y + textRenderer.fontHeight, -1, true); + } else { + MutableText formatted = Text.literal(" ").append(Text.translatable( "skyblocker.events.tab.endsIn", Utils.getDurationText((int) (events.peekFirst().start() + events.peekFirst().duration() - time)))).formatted(Formatting.GREEN); + context.drawText(textRenderer, formatted, x, y + textRenderer.fontHeight, -1, true); + } + + } + + public int getHeight() { + return 20; + } + + public boolean isMouseOver(int mouseX, int mouseY, int x, int y) { + return mouseX >= x && mouseX <= x + 131 && mouseY >= y && mouseY <= y+getHeight(); + } + + public List<TooltipComponent> getTooltip() { + List<TooltipComponent> components = new ArrayList<>(); + if (events.peekFirst() == null) return components; + if (eventName.equals(EventNotifications.JACOBS)) { + components.add(new JacobsTooltip(events.peekFirst().extras())); + } + //noinspection DataFlowIssue + if (events.peekFirst().warpCommand() != null) { + components.add(TooltipComponent.of(Text.translatable("skyblocker.events.tab.clickToWarp").formatted(Formatting.ITALIC).asOrderedText())); + } + + return components; + } + + public @Nullable String getWarpCommand() { + if (events.isEmpty()) return null; + return events.peek().warpCommand(); + } + } + + private record JacobsTooltip(String[] crops) implements TooltipComponent { + + private static final ItemStack BARRIER = new ItemStack(Items.BARRIER); + + @Override + public int getHeight() { + return 20; + } + + @Override + public int getWidth(TextRenderer textRenderer) { + return 16 * 3 + 4; + } + + @Override + public void drawItems(TextRenderer textRenderer, int x, int y, DrawContext context) { + for (int i = 0; i < crops.length; i++) { + String crop = crops[i]; + context.drawItem(JacobsContestWidget.FARM_DATA.getOrDefault(crop, BARRIER), x + 18 * i, y + 2); + } + } + + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/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/Utils.java b/src/main/java/de/hysky/skyblocker/utils/Utils.java index 62a3b897..7c28294f 100644 --- a/src/main/java/de/hysky/skyblocker/utils/Utils.java +++ b/src/main/java/de/hysky/skyblocker/utils/Utils.java @@ -19,6 +19,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; @@ -355,6 +356,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/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); + } +} diff --git a/src/main/resources/assets/skyblocker/lang/en_us.json b/src/main/resources/assets/skyblocker/lang/en_us.json index 57abdc46..9361c941 100644 --- a/src/main/resources/assets/skyblocker/lang/en_us.json +++ b/src/main/resources/assets/skyblocker/lang/en_us.json @@ -396,6 +396,14 @@ "skyblocker.config.chat.filter.hideToggleSkyMall": "Hide Toggle Sky Mall Messages", "skyblocker.config.chat.filter.hideToggleSkyMall.@Tooltip": "Hides those pesky messages telling you to disable the Sky Mall HOTM perk when you want it enabled!", + "skyblocker.config.eventNotifications": "Event Notifications", + + "skyblocker.config.eventNotifications.monologue": "can you pls log onto skyblock rq pls? that would be cool cuz like if you are seeing dis then it means that ur config either got cleared or that this is ur first time using the mod if so then thanks for choosing it and hopefully you enjoy it! so yea this is where you will be able to set reminders for all events in skyblock they will be added as you encounter so you first need to log onto skyblock so yea hope you enjoy the mod and all that", + "skyblocker.config.eventNotifications.notificationSound": "Notification Sound", + "skyblocker.config.eventNotifications.@Tooltip[0]": "Configure how much time before an event you will be reminded with a notification toast! For example if you set '5m' and '30s' in the list, you will receive a notification 5 minutes before and another 30 seconds before an event starts.", + "skyblocker.config.eventNotifications.@Tooltip[1]": "The order doesn't matter. If you want to disable notifications, just empty the list.", + "skyblocker.config.eventNotifications.@Tooltip[2]": "This list will modify the '%s' event", + "skyblocker.config.mining": "Mining", "skyblocker.config.mining.commissionWaypoints": "Commission Waypoints", @@ -697,6 +705,14 @@ "skyblocker.end.hud.protectorLocations.rightFront": "Right Front", "skyblocker.end.hud.protectorLocations.rightBack": "Right Back", + "skyblocker.events.startsNow": "%s starts now!", + "skyblocker.events.startsSoon": "%s starts soon!", + "skyblocker.events.tab.endsIn": "Ends in %s", + "skyblocker.events.tab.clickToWarp": "Click to warp!", + "skyblocker.events.tab.noMore": "No more this year!", + "skyblocker.events.tab.startsIn": "Starts in %s", + + "skyblocker.garden.hud.mouseLocked": "Mouse locked.", "skyblocker.fishing.reelNow": "Reel in now!", diff --git a/src/main/resources/assets/skyblocker/textures/gui/sprites/notification.png b/src/main/resources/assets/skyblocker/textures/gui/sprites/notification.png Binary files differnew file mode 100644 index 00000000..8f272cd7 --- /dev/null +++ b/src/main/resources/assets/skyblocker/textures/gui/sprites/notification.png diff --git a/src/main/resources/assets/skyblocker/textures/gui/sprites/notification.png.mcmeta b/src/main/resources/assets/skyblocker/textures/gui/sprites/notification.png.mcmeta new file mode 100644 index 00000000..c514d54c --- /dev/null +++ b/src/main/resources/assets/skyblocker/textures/gui/sprites/notification.png.mcmeta @@ -0,0 +1,10 @@ +{ + "gui": { + "scaling": { + "type": "nine_slice", + "width": 160, + "height": 32, + "border": 4 + } + } +}
\ No newline at end of file |