diff --git a/src/main/java/at/hannibal2/skyhanni/config/commands/Commands.kt b/src/main/java/at/hannibal2/skyhanni/config/commands/Commands.kt index 89f314135541..ada100aac60a 100644 --- a/src/main/java/at/hannibal2/skyhanni/config/commands/Commands.kt +++ b/src/main/java/at/hannibal2/skyhanni/config/commands/Commands.kt @@ -56,6 +56,7 @@ import at.hannibal2.skyhanni.features.garden.fortuneguide.FFGuideGUI import at.hannibal2.skyhanni.features.garden.pests.PestFinder import at.hannibal2.skyhanni.features.garden.pests.PestProfitTracker import at.hannibal2.skyhanni.features.garden.visitor.GardenVisitorDropStatistics +import at.hannibal2.skyhanni.features.inventory.caketracker.CakeTracker import at.hannibal2.skyhanni.features.inventory.chocolatefactory.ChocolateFactoryStrayTracker import at.hannibal2.skyhanni.features.inventory.experimentationtable.ExperimentsProfitTracker import at.hannibal2.skyhanni.features.mining.KingTalismanHelper @@ -396,6 +397,11 @@ object Commands { category = CommandCategory.USERS_RESET callback { ExcavatorProfitTracker.resetCommand() } } + event.register("shresetcaketracker") { + description = "Resets the New Year Cake Tracker" + category = CommandCategory.USERS_RESET + callback { CakeTracker.resetCommand() } + } // non trackers event.register("shresetghostcounter") { diff --git a/src/main/java/at/hannibal2/skyhanni/config/features/inventory/InventoryConfig.java b/src/main/java/at/hannibal2/skyhanni/config/features/inventory/InventoryConfig.java index 71ab7c792147..65ea4cbc33e5 100644 --- a/src/main/java/at/hannibal2/skyhanni/config/features/inventory/InventoryConfig.java +++ b/src/main/java/at/hannibal2/skyhanni/config/features/inventory/InventoryConfig.java @@ -9,6 +9,7 @@ import at.hannibal2.skyhanni.config.features.itemability.ItemAbilityConfig; import at.hannibal2.skyhanni.config.features.misc.EstimatedItemValueConfig; import at.hannibal2.skyhanni.config.features.misc.PocketSackInASackConfig; +import at.hannibal2.skyhanni.features.inventory.caketracker.CakeTrackerConfig; import com.google.gson.annotations.Expose; import io.github.notenoughupdates.moulconfig.annotations.Accordion; import io.github.notenoughupdates.moulconfig.annotations.Category; @@ -134,6 +135,11 @@ public class InventoryConfig { @Accordion public PageScrollingConfig pageScrolling = new PageScrollingConfig(); + @Expose + @ConfigOption(name = "Cake Tracker", desc = "") + @Accordion + public CakeTrackerConfig cakeTracker = new CakeTrackerConfig(); + @Expose @ConfigOption(name = "Magical Power Display", desc = "") @Accordion diff --git a/src/main/java/at/hannibal2/skyhanni/config/storage/ProfileSpecificStorage.java b/src/main/java/at/hannibal2/skyhanni/config/storage/ProfileSpecificStorage.java index 4ad2c8eb300a..fea03be7ca2f 100644 --- a/src/main/java/at/hannibal2/skyhanni/config/storage/ProfileSpecificStorage.java +++ b/src/main/java/at/hannibal2/skyhanni/config/storage/ProfileSpecificStorage.java @@ -31,6 +31,7 @@ import at.hannibal2.skyhanni.features.garden.pests.PestProfitTracker; import at.hannibal2.skyhanni.features.garden.pests.VinylType; import at.hannibal2.skyhanni.features.garden.visitor.VisitorReward; +import at.hannibal2.skyhanni.features.inventory.caketracker.CakeTracker; import at.hannibal2.skyhanni.features.inventory.chocolatefactory.ChocolateFactoryStrayTracker; import at.hannibal2.skyhanni.features.inventory.experimentationtable.ExperimentsProfitTracker; import at.hannibal2.skyhanni.features.inventory.wardrobe.WardrobeAPI; @@ -516,6 +517,9 @@ public static class GhostCounter { } + @Expose + public CakeTracker.Data cakeTracker = new CakeTracker.Data(); + @Expose public PowderTracker.Data powderTracker = new PowderTracker.Data(); diff --git a/src/main/java/at/hannibal2/skyhanni/features/inventory/caketracker/CakeTracker.kt b/src/main/java/at/hannibal2/skyhanni/features/inventory/caketracker/CakeTracker.kt new file mode 100644 index 000000000000..861e089b6f70 --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/features/inventory/caketracker/CakeTracker.kt @@ -0,0 +1,373 @@ +package at.hannibal2.skyhanni.features.inventory.caketracker + +import at.hannibal2.skyhanni.SkyHanniMod +import at.hannibal2.skyhanni.data.ProfileStorageData +import at.hannibal2.skyhanni.events.GuiContainerEvent +import at.hannibal2.skyhanni.events.GuiRenderEvent +import at.hannibal2.skyhanni.events.InventoryCloseEvent +import at.hannibal2.skyhanni.events.InventoryFullyOpenedEvent +import at.hannibal2.skyhanni.events.SecondPassedEvent +import at.hannibal2.skyhanni.features.inventory.caketracker.CakeTrackerConfig.CakeTrackerDisplayOrderType +import at.hannibal2.skyhanni.features.inventory.caketracker.CakeTrackerConfig.CakeTrackerDisplayType +import at.hannibal2.skyhanni.features.inventory.patternGroup +import at.hannibal2.skyhanni.skyhannimodule.SkyHanniModule +import at.hannibal2.skyhanni.utils.CollectionUtils.addSearchString +import at.hannibal2.skyhanni.utils.ColorUtils.toChromaColor +import at.hannibal2.skyhanni.utils.HypixelCommands +import at.hannibal2.skyhanni.utils.InventoryUtils +import at.hannibal2.skyhanni.utils.LorenzUtils +import at.hannibal2.skyhanni.utils.NumberUtil.formatInt +import at.hannibal2.skyhanni.utils.RegexUtils.groupOrNull +import at.hannibal2.skyhanni.utils.RegexUtils.matchMatcher +import at.hannibal2.skyhanni.utils.RegexUtils.matches +import at.hannibal2.skyhanni.utils.RenderUtils.highlight +import at.hannibal2.skyhanni.utils.SimpleTimeMark +import at.hannibal2.skyhanni.utils.SkyBlockTime +import at.hannibal2.skyhanni.utils.renderables.Renderable +import at.hannibal2.skyhanni.utils.renderables.Searchable +import at.hannibal2.skyhanni.utils.renderables.toSearchable +import at.hannibal2.skyhanni.utils.tracker.SkyHanniTracker +import at.hannibal2.skyhanni.utils.tracker.TrackerData +import com.google.gson.annotations.Expose +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent +import kotlin.time.Duration.Companion.milliseconds + +@SkyHanniModule +object CakeTracker { + + private fun getCakeTrackerData() = ProfileStorageData.profileSpecific?.cakeTracker + private val config get() = SkyHanniMod.feature.inventory.cakeTracker + private var currentYear = 0 + + private var inCakeBag = false + private var inCakeInventory = false + private var timeOpenedCakeInventory = SimpleTimeMark.farPast() + private var inAuctionHouse = false + private var unobtainedCakesDisplayed = false + private var searchingForCakes = false + private var knownCakesInCurrentInventory = mutableListOf() + + /** + * REGEX-TEST: §cNew Year Cake (Year 360) + * REGEX-TEST: §cNew Year Cake (Year 1,000) + * REGEX-TEST: §f§f§cNew Year Cake (Year 330) + */ + private val cakeNamePattern by patternGroup.pattern( + "cake.name", + "(?:§f§f)?§cNew Year Cake \\(Year (?[\\d,]*)\\)", + ) + + /** + * REGEX-TEST: Ender Chest (2/9) + * REGEX-TEST: Jumbo Backpack (Slot #6) + * REGEX-TEST: New Year Cake Bag + */ + private val cakeContainerPattern by patternGroup.pattern( + "cake.container", + "^(Ender Chest \\(\\d{1,2}/\\d{1,2}\\)|.*Backpack(?:§r)? \\(Slot #\\d{1,2}\\)|New Year Cake Bag)$", + ) + + /** + * REGEX-TEST: New Year Cake Bag + */ + private val cakeBagPattern by patternGroup.pattern( + "cake.bag", + "^New Year Cake Bag$", + ) + + /** + * REGEX-TEST: Auctions Browser + * REGEX-TEST: Auctions: "Test" + */ + private val auctionBrowserPattern by patternGroup.pattern( + "auction.search", + "^(Auctions Browser|Auctions: \".*)$", + ) + + /** + * REGEX-TEST: Auctions: "New Year C + */ + private val auctionCakeSearchPattern by patternGroup.pattern( + "auction.cakesearch", + "^Auctions: \"New Year C.*$", + ) + + private val tracker = SkyHanniTracker("New Year Cake Tracker", { Data() }, { it.cakeTracker }) + { drawDisplay(it) } + + class Data : TrackerData() { + override fun reset() { + cakesOwned.clear() + } + + @Expose + var cakesOwned: MutableList = mutableListOf() + + @Expose + var cakesMissing: MutableList = mutableListOf() + } + + private fun addCake(cakeYear: Int) { + val cakeTrackerData = getCakeTrackerData() ?: return + if (!cakeTrackerData.cakesOwned.contains(cakeYear)) { + tracker.modify { + it.cakesOwned.add(cakeYear) + } + } + recalculateMissingCakes() + } + + private fun removeCake(cakeYear: Int) { + val cakeTrackerData = getCakeTrackerData() ?: return + if (cakeTrackerData.cakesOwned.contains(cakeYear)) { + tracker.modify { + it.cakesOwned.remove(cakeYear) + } + } + recalculateMissingCakes() + } + + private fun isEnabled() = LorenzUtils.inSkyBlock && config.enabled + + @SubscribeEvent + fun onBackgroundDraw(event: GuiRenderEvent.ChestGuiOverlayRenderEvent) { + if (!isEnabled()) return + if (inCakeBag || (inAuctionHouse && (unobtainedCakesDisplayed || searchingForCakes))) { + tracker.renderDisplay(config.cakeTrackerPosition, displayModeToggleable = false) + } + } + + @SubscribeEvent + fun onBackgroundDraw(event: GuiContainerEvent.BackgroundDrawnEvent) { + if (!isEnabled()) return + if (inAuctionHouse) { + unobtainedCakesDisplayed = getCakeTrackerData()?.let { data -> + InventoryUtils.getItemsInOpenChest().onEach { cakeItem -> + cakeNamePattern.matchMatcher(cakeItem.stack.displayName) { + group("year").toInt().takeIf { + it !in data.cakesOwned + }?.let { + cakeItem highlight config.auctionHighlightColor.toChromaColor() + } + } + }.isNotEmpty() + } ?: false + } + if (inCakeInventory) checkInventoryCakes() + } + + @SubscribeEvent + fun onInventoryOpen(event: InventoryFullyOpenedEvent) { + if (!isEnabled()) return + knownCakesInCurrentInventory.clear() + val inventoryName = event.inventoryName + if (cakeContainerPattern.matches(inventoryName)) { + if (cakeBagPattern.matches(inventoryName)) inCakeBag = true + knownCakesInCurrentInventory = event.inventoryItems.values.mapNotNull { item -> + cakeNamePattern.matchMatcher(item.displayName) { + groupOrNull("year")?.formatInt()?.let { + addCake(it) + it + } + } + }.toMutableList() + inCakeInventory = true + timeOpenedCakeInventory = SimpleTimeMark.now() + tracker.firstUpdate() + } + if (auctionBrowserPattern.matches(inventoryName)) { + inAuctionHouse = true + searchingForCakes = auctionCakeSearchPattern.matches(inventoryName) + } else inAuctionHouse = false + } + + @SubscribeEvent + fun onInventoryClose(event: InventoryCloseEvent) { + inCakeBag = false + inCakeInventory = false + knownCakesInCurrentInventory.clear() + inAuctionHouse = false + unobtainedCakesDisplayed = false + searchingForCakes = false + } + + @SubscribeEvent + fun onSecondPassed(event: SecondPassedEvent) { + if (!isEnabled()) return + val sbTimeNow = SkyBlockTime.now() + if (currentYear == sbTimeNow.year) return + if (sbTimeNow.month == 12 && sbTimeNow.day >= 29) { + currentYear = sbTimeNow.year + recalculateMissingCakes() + } else currentYear = sbTimeNow.year - 1 + } + + private fun checkInventoryCakes() { + if (timeOpenedCakeInventory.passedSince() < 500.milliseconds) return + val currentYears = InventoryUtils.getItemsInOpenChest().mapNotNull { item -> + cakeNamePattern.matchMatcher(item.stack.displayName) { + group("year")?.toInt() + } + } + + val addedYears = currentYears.filter { it !in knownCakesInCurrentInventory } + val removedYears = knownCakesInCurrentInventory.filter { it !in currentYears } + + addedYears.forEach(::addCake) + removedYears.forEach(::removeCake) + + if (addedYears.isNotEmpty() || removedYears.isNotEmpty()) { + knownCakesInCurrentInventory = currentYears.toMutableList() + } + } + + private fun recalculateMissingCakes() { + val cakeTrackerData = getCakeTrackerData() ?: return + tracker.modify { + it.cakesMissing = (1..currentYear).filterNot { + year -> cakeTrackerData.cakesOwned.contains(year) + }.toMutableList() + } + } + + private class CakeRange(var start: Int, var end: Int = 0) { + fun getRenderable(displayType: CakeTrackerDisplayType): Renderable { + val colorCode = + if (displayType == CakeTrackerDisplayType.OWNED_CAKES) "§a" + else "§c" + val stringRenderable = + Renderable.string( + if (end != 0) "§fYears $colorCode$start§f-$colorCode$end" + else "§fYear $colorCode$start" + ) + return if (displayType == CakeTrackerDisplayType.MISSING_CAKES) Renderable.link( + stringRenderable, + { HypixelCommands.auctionHouseSearch("New Year Cake (Year $start)") }, + ) else stringRenderable + } + } + + private fun setDisplayType(type: CakeTrackerDisplayType) { + val cakeTrackerData = getCakeTrackerData() ?: return + config.displayType = type + drawDisplay(cakeTrackerData) + tracker.update() + } + + private fun buildDisplayTypeToggle(): Renderable = Renderable.horizontalContainer( + buildList { + val ownedString = + if (config.displayType == CakeTrackerDisplayType.OWNED_CAKES) "§7§l[§r §a§nOwned§r §7§l]" + else "§aOwned" + val missingString = + if (config.displayType == CakeTrackerDisplayType.MISSING_CAKES) "§7§l[§r §c§nMissing§r §7§l]" + else "§cMissing" + + add( + Renderable.optionalLink( + ownedString, + { setDisplayType(CakeTrackerDisplayType.OWNED_CAKES) }, + condition = { config.displayType != CakeTrackerDisplayType.OWNED_CAKES }, + ) + ) + add(Renderable.string(" §7§l- §r")) + add( + Renderable.optionalLink( + missingString, + { setDisplayType(CakeTrackerDisplayType.MISSING_CAKES) }, + condition = { config.displayType != CakeTrackerDisplayType.MISSING_CAKES }, + ) + ) + } + ) + + private fun setDisplayOrderType(type: CakeTrackerDisplayOrderType) { + val cakeTrackerData = getCakeTrackerData() ?: return + config.displayOrderType = type + drawDisplay(cakeTrackerData) + tracker.update() + } + + private fun buildOrderTypeToggle(): Renderable = Renderable.horizontalContainer( + buildList { + val newestString = + if (config.displayOrderType == CakeTrackerDisplayOrderType.NEWEST_FIRST) "§7§l[§r §a§nNewest First§r §7§l]" + else "§aNewest First" + val oldestString = + if (config.displayOrderType == CakeTrackerDisplayOrderType.OLDEST_FIRST) "§7§l[§r §c§nOldest First§r §7§l]" + else "§cOldest First" + + add( + Renderable.optionalLink( + newestString, + { setDisplayOrderType(CakeTrackerDisplayOrderType.NEWEST_FIRST) }, + condition = { config.displayOrderType != CakeTrackerDisplayOrderType.NEWEST_FIRST }, + ) + ) + add(Renderable.string(" §7§l- §r")) + add( + Renderable.optionalLink( + oldestString, + { setDisplayOrderType(CakeTrackerDisplayOrderType.OLDEST_FIRST) }, + condition = { config.displayOrderType != CakeTrackerDisplayOrderType.OLDEST_FIRST }, + ) + ) + } + ) + + private fun drawDisplay(data: Data): List = buildList { + add( + Renderable.hoverTips( + "§c§lNew §f§lYear §c§lCake §f§lTracker", + tips = listOf("§aHave§7: §a${data.cakesOwned.count()}§7, §cMissing§7: §c${data.cakesMissing.count()}"), + ).toSearchable(), + ) + add(buildDisplayTypeToggle().toSearchable("Display Type")) + add(buildOrderTypeToggle().toSearchable("Order Type")) + + val cakeList = when (config.displayType) { + CakeTrackerDisplayType.OWNED_CAKES -> data.cakesOwned + CakeTrackerDisplayType.MISSING_CAKES -> data.cakesMissing + null -> data.cakesMissing + } + + if (cakeList.isEmpty()) { + val colorCode = if (config.displayType == CakeTrackerDisplayType.OWNED_CAKES) "§c" else "§a" + val verbiage = if (config.displayType == CakeTrackerDisplayType.OWNED_CAKES) "missing" else "owned" + addSearchString("$colorCode§lAll cakes $verbiage!") + } else { + val sortedCakes = when (config.displayOrderType) { + CakeTrackerDisplayOrderType.OLDEST_FIRST -> cakeList.sorted() + CakeTrackerDisplayOrderType.NEWEST_FIRST -> cakeList.sortedDescending() + null -> cakeList + } + + // Combine consecutive years into ranges + val cakeRanges = mutableListOf() + var start = sortedCakes.first() + var end = start + + for (i in 1 until sortedCakes.size) { + if ((config.displayOrderType == CakeTrackerDisplayOrderType.OLDEST_FIRST && sortedCakes[i] == end + 1) || + (config.displayOrderType == CakeTrackerDisplayOrderType.NEWEST_FIRST && sortedCakes[i] == end - 1) + ) { + end = sortedCakes[i] + } else { + if (start != end) cakeRanges.add(CakeRange(start, end)) + else cakeRanges.add(CakeRange(start)) + start = sortedCakes[i] + end = start + } + } + + if (start != end) cakeRanges.add(CakeRange(start, end)) + else cakeRanges.add(CakeRange(start)) + + cakeRanges.forEach { add(it.getRenderable(config.displayType).toSearchable("${it.start}")) } + } + } + + fun resetCommand() { + tracker.resetCommand() + } +} diff --git a/src/main/java/at/hannibal2/skyhanni/features/inventory/caketracker/CakeTrackerConfig.java b/src/main/java/at/hannibal2/skyhanni/features/inventory/caketracker/CakeTrackerConfig.java new file mode 100644 index 000000000000..dc255853736b --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/features/inventory/caketracker/CakeTrackerConfig.java @@ -0,0 +1,74 @@ +package at.hannibal2.skyhanni.features.inventory.caketracker; + +import at.hannibal2.skyhanni.config.FeatureToggle; +import at.hannibal2.skyhanni.config.core.config.Position; +import at.hannibal2.skyhanni.utils.LorenzColor; +import com.google.gson.annotations.Expose; +import io.github.notenoughupdates.moulconfig.annotations.ConfigEditorBoolean; +import io.github.notenoughupdates.moulconfig.annotations.ConfigEditorColour; +import io.github.notenoughupdates.moulconfig.annotations.ConfigEditorDropdown; +import io.github.notenoughupdates.moulconfig.annotations.ConfigLink; +import io.github.notenoughupdates.moulconfig.annotations.ConfigOption; + +public class CakeTrackerConfig { + + @Expose + @ConfigOption(name = "New Year Cake Tracker", desc = "Track which Cakes you have/need. §cWill not fully work with NEU Storage Overlay.") + @ConfigEditorBoolean + @FeatureToggle + public boolean enabled = false; + + @Expose + @ConfigLink(owner = CakeTrackerConfig.class, field = "enabled") + public Position cakeTrackerPosition = new Position(300, 300, false, true); + + @Expose + @ConfigOption(name = "Display Mode", desc = "Which cakes the tracker should display.") + @ConfigEditorDropdown + public CakeTrackerDisplayType displayType = CakeTrackerDisplayType.MISSING_CAKES; + + public enum CakeTrackerDisplayType { + MISSING_CAKES("§cMissing Cakes"), + OWNED_CAKES("§aOwned Cakes") + ; + + private final String name; + + CakeTrackerDisplayType(String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } + } + + @Expose + @ConfigOption(name = "Display Order", desc = "What order the tracker should display cakes in.") + @ConfigEditorDropdown + public CakeTrackerDisplayOrderType displayOrderType = CakeTrackerDisplayOrderType.OLDEST_FIRST; + + public enum CakeTrackerDisplayOrderType { + + OLDEST_FIRST("§cOldest Cakes First"), + NEWEST_FIRST("§dNewest Cakes First") + ; + + private final String name; + + CakeTrackerDisplayOrderType(String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } + } + + @Expose + @ConfigOption(name = "Auction Highlight Color", desc = "The color that should be used to highlight unobtained cakes in the auction house.") + @ConfigEditorColour + public String auctionHighlightColor = LorenzColor.RED.toConfigColor(); +} diff --git a/src/main/java/at/hannibal2/skyhanni/utils/HypixelCommands.kt b/src/main/java/at/hannibal2/skyhanni/utils/HypixelCommands.kt index c148e93e80fd..1219cb280544 100644 --- a/src/main/java/at/hannibal2/skyhanni/utils/HypixelCommands.kt +++ b/src/main/java/at/hannibal2/skyhanni/utils/HypixelCommands.kt @@ -154,6 +154,10 @@ object HypixelCommands { send("chatprompt $prompt") } + fun auctionHouseSearch(query: String) { + send("ahs $query") + } + fun callback(uuid: String) { send("cb $uuid") } diff --git a/src/main/java/at/hannibal2/skyhanni/utils/tracker/SkyHanniTracker.kt b/src/main/java/at/hannibal2/skyhanni/utils/tracker/SkyHanniTracker.kt index eea93de044e5..cf76cd47d902 100644 --- a/src/main/java/at/hannibal2/skyhanni/utils/tracker/SkyHanniTracker.kt +++ b/src/main/java/at/hannibal2/skyhanni/utils/tracker/SkyHanniTracker.kt @@ -71,7 +71,7 @@ open class SkyHanniTracker( update() } - fun renderDisplay(position: Position) { + fun renderDisplay(position: Position, displayModeToggleable: Boolean = true) { if (config.hideInEstimatedItemValue && EstimatedItemValue.isCurrentlyShowing()) return val currentlyOpen = Minecraft.getMinecraft().currentScreen?.let { it is GuiInventory || it is GuiChest } ?: false @@ -87,7 +87,7 @@ open class SkyHanniTracker( display = getSharedTracker()?.let { val data = it.get(getDisplayMode()) val searchables = drawDisplay(data) - buildFinalDisplay(searchables.buildSearchBox(textInput)) + buildFinalDisplay(searchables.buildSearchBox(textInput), displayModeToggleable) } ?: emptyList() dirty = false } @@ -99,14 +99,14 @@ open class SkyHanniTracker( dirty = true } - private fun buildFinalDisplay(searchBox: Renderable) = buildList { + private fun buildFinalDisplay(searchBox: Renderable, displayModeToggleable: Boolean) = buildList { add(searchBox) if (isEmpty()) return@buildList - if (inventoryOpen) { + if (inventoryOpen && displayModeToggleable) { add(buildDisplayModeView()) - } - if (inventoryOpen && getDisplayMode() == DisplayMode.SESSION) { - add(buildSessionResetButton()) + if (getDisplayMode() == DisplayMode.SESSION) { + add(buildSessionResetButton()) + } } }