diff options
Diffstat (limited to 'src/main/java')
18 files changed, 963 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); + } +} |