From 80a6728abdd34d80495bfd18f0250bbf59865a25 Mon Sep 17 00:00:00 2001 From: Kaeso <24925519+ptlthg@users.noreply.github.com> Date: Sun, 27 Aug 2023 16:32:00 -0400 Subject: Merge pull request #335 * Sync Jacob Contests --- src/main/java/at/hannibal2/skyhanni/SkyHanniMod.kt | 2 +- .../java/at/hannibal2/skyhanni/config/Storage.java | 3 + .../hannibal2/skyhanni/config/commands/Commands.kt | 3 +- .../skyhanni/config/features/GardenConfig.java | 12 + .../features/garden/GardenNextJacobContest.kt | 270 +++++++++++++++++---- .../java/at/hannibal2/skyhanni/utils/APIUtil.kt | 35 +++ 6 files changed, 281 insertions(+), 44 deletions(-) (limited to 'src/main/java/at/hannibal2') diff --git a/src/main/java/at/hannibal2/skyhanni/SkyHanniMod.kt b/src/main/java/at/hannibal2/skyhanni/SkyHanniMod.kt index 927eabb46..3e8aae8f2 100644 --- a/src/main/java/at/hannibal2/skyhanni/SkyHanniMod.kt +++ b/src/main/java/at/hannibal2/skyhanni/SkyHanniMod.kt @@ -285,7 +285,7 @@ class SkyHanniMod { loadModule(DicerRngDropCounter()) loadModule(CropMoneyDisplay) loadModule(JacobFarmingContestsInventory()) - loadModule(GardenNextJacobContest()) + loadModule(GardenNextJacobContest) loadModule(WrongFungiCutterWarning()) loadModule(FarmingArmorDrops()) loadModule(JoinCrystalHollows()) diff --git a/src/main/java/at/hannibal2/skyhanni/config/Storage.java b/src/main/java/at/hannibal2/skyhanni/config/Storage.java index 79731f069..a2af0ed79 100644 --- a/src/main/java/at/hannibal2/skyhanni/config/Storage.java +++ b/src/main/java/at/hannibal2/skyhanni/config/Storage.java @@ -23,6 +23,9 @@ public class Storage { @Expose public Map> gardenJacobFarmingContestTimes = new HashMap<>(); + @Expose + public Boolean contestSendingAsked = false; + @Expose public Map players = new HashMap<>(); 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 118af32e7..0b4f8c6e9 100644 --- a/src/main/java/at/hannibal2/skyhanni/config/commands/Commands.kt +++ b/src/main/java/at/hannibal2/skyhanni/config/commands/Commands.kt @@ -13,6 +13,7 @@ import at.hannibal2.skyhanni.features.fame.AccountUpgradeReminder import at.hannibal2.skyhanni.features.fame.CityProjectFeatures import at.hannibal2.skyhanni.features.garden.GardenAPI import at.hannibal2.skyhanni.features.garden.GardenCropTimeCommand +import at.hannibal2.skyhanni.features.garden.GardenNextJacobContest import at.hannibal2.skyhanni.features.garden.composter.ComposterOverlay import at.hannibal2.skyhanni.features.garden.farming.CropMoneyDisplay import at.hannibal2.skyhanni.features.garden.farming.CropSpeedMeter @@ -230,8 +231,8 @@ object Commands { registerCommand("shshareinquis", "") { InquisitorWaypointShare.sendInquisitor() } registerCommand("shcopyerror", "") { CopyErrorCommand.command(it) } registerCommand("shstopcityprojectreminder", "") { CityProjectFeatures.disable() } + registerCommand("shsendcontests", "") { GardenNextJacobContest.shareContestConfirmed(it) } registerCommand("shstopaccountupgradereminder", "") { AccountUpgradeReminder.disable() } - } private fun commandHelp(args: Array) { diff --git a/src/main/java/at/hannibal2/skyhanni/config/features/GardenConfig.java b/src/main/java/at/hannibal2/skyhanni/config/features/GardenConfig.java index b3a5abd27..1d30c2998 100644 --- a/src/main/java/at/hannibal2/skyhanni/config/features/GardenConfig.java +++ b/src/main/java/at/hannibal2/skyhanni/config/features/GardenConfig.java @@ -937,6 +937,18 @@ public class GardenConfig { @ConfigAccordionId(id = 14) public boolean nextJacobContestOtherGuis = false; + @Expose + @ConfigOption(name = "Fetch Contests", desc = "Automatically fetch contests from elitebot.dev for the current year if they're uploaded already.") + @ConfigEditorBoolean + @ConfigAccordionId(id = 14) + public boolean nextJacobContestsFetchAutomatically = true; + + @Expose + @ConfigOption(name = "Share Contests", desc = "Share the list of upcoming contests to elitebot.dev for everyone else to then fetch automatically.") + @ConfigEditorDropdown(values = { "Ask When Needed", "Share Automatically", "Disabled" }) + @ConfigAccordionId(id = 14) + public int nextJacobContestsShareAutomatically = 0; + @Expose @ConfigOption(name = "Warning", desc = "Show a warning shortly before a new Jacob's contest starts.") @ConfigEditorBoolean diff --git a/src/main/java/at/hannibal2/skyhanni/features/garden/GardenNextJacobContest.kt b/src/main/java/at/hannibal2/skyhanni/features/garden/GardenNextJacobContest.kt index 1525a2209..9ec766953 100644 --- a/src/main/java/at/hannibal2/skyhanni/features/garden/GardenNextJacobContest.kt +++ b/src/main/java/at/hannibal2/skyhanni/features/garden/GardenNextJacobContest.kt @@ -4,6 +4,7 @@ import at.hannibal2.skyhanni.SkyHanniMod import at.hannibal2.skyhanni.data.TitleUtils import at.hannibal2.skyhanni.events.* import at.hannibal2.skyhanni.features.garden.GardenAPI.addCropIcon +import at.hannibal2.skyhanni.utils.APIUtil import at.hannibal2.skyhanni.utils.ItemUtils.getLore import at.hannibal2.skyhanni.utils.ItemUtils.name import at.hannibal2.skyhanni.utils.LorenzUtils @@ -13,8 +14,11 @@ import at.hannibal2.skyhanni.utils.SoundUtils import at.hannibal2.skyhanni.utils.StringUtils.matchMatcher import at.hannibal2.skyhanni.utils.StringUtils.removeColor import at.hannibal2.skyhanni.utils.TimeUtils +import com.google.gson.Gson import io.github.moulberry.notenoughupdates.util.SkyBlockTime +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import net.minecraft.item.ItemStack import net.minecraftforge.fml.common.eventhandler.SubscribeEvent import org.lwjgl.opengl.Display @@ -26,7 +30,7 @@ import javax.swing.JFrame import javax.swing.JOptionPane import javax.swing.UIManager -class GardenNextJacobContest { +object GardenNextJacobContest { private var display = emptyList() private var simpleDisplay = emptyList() private var contests = mutableMapOf() @@ -35,9 +39,16 @@ class GardenNextJacobContest { private val patternMonth = "(?.*), Year (?.*)".toPattern() private val patternCrop = "§e○ §7(?.*)".toPattern() - private val maxContestsPerYear = 124 - private val contestDuration = 1_000 * 60 * 20 + private const val maxContestsPerYear = 124 + private const val contestDuration = 1_000 * 60 * 20 private var lastWarningTime = 0L + private var loadedContestsYear = -1 + private var nextContestsAvailableAt = -1L + + private var lastFetchAttempted = 0L + private var isFetchingContests = false + private var fetchedFromElite = false + private var isSendingContests = false @SubscribeEvent fun onTabListUpdate(event: TabListUpdateEvent) { @@ -102,38 +113,63 @@ class GardenNextJacobContest { } private fun readCalendar(items: Collection, year: Int, month: Int) { - if (contests.isNotEmpty()) { - val contest = contests.values.first() - val endTime = contest.endTime + if (contests.isNotEmpty() && loadedContestsYear != year) { + val endTime = contests.values.first().endTime val lastYear = SkyBlockTime.fromInstant(Instant.ofEpochMilli(endTime)).year if (year != lastYear) { contests.clear() - LorenzUtils.chat("§e[SkyHanni] New year detected, open all calendar months again!") + } + // Contests are available now, make sure system knows this + if (nextContestsAvailableAt > System.currentTimeMillis()) { + nextContestsAvailableAt = System.currentTimeMillis() - 1 + fetchContestsIfAble() + } + if (nextContestsAvailableAt == -1L) { + nextContestsAvailableAt = System.currentTimeMillis() - 1 + fetchContestsIfAble() } } - if (contests.size < maxContestsPerYear) { - for (item in items) { - val lore = item.getLore() - if (!lore.any { it.contains("§6§eJacob's Farming Contest") }) continue - - val name = item.name ?: continue - val matcherDay = patternDay.matcher(name) - if (!matcherDay.matches()) continue - - val day = matcherDay.group("day").toInt() - val startTime = SkyBlockTime(year, month, day).toMillis() - val crops = mutableListOf() - for (line in lore) { - val matcherCrop = patternCrop.matcher(line) - if (!matcherCrop.matches()) continue - crops.add(CropType.getByName(matcherCrop.group("crop"))) - } - val contest = FarmingContest(startTime + contestDuration, crops) - contests[startTime] = contest + // Skip if contests are already loaded for this year + if (contests.size == maxContestsPerYear) return + + // Manually loading contests + for (item in items) { + val lore = item.getLore() + if (!lore.any { it.contains("§6§eJacob's Farming Contest") }) continue + + val name = item.name ?: continue + val matcherDay = patternDay.matcher(name) + if (!matcherDay.matches()) continue + + val day = matcherDay.group("day").toInt() + val startTime = SkyBlockTime(year, month, day).toMillis() + + val crops = mutableListOf() + for (line in lore) { + val matcherCrop = patternCrop.matcher(line) + if (!matcherCrop.matches()) continue + crops.add(CropType.getByName(matcherCrop.group("crop"))) } + + contests[startTime] = FarmingContest(startTime + contestDuration, crops) } + // If contests were just fully saved + if (contests.size == maxContestsPerYear) { + nextContestsAvailableAt = SkyBlockTime(SkyBlockTime.now().year + 1, 1, 2).toMillis() + + if (isSendEnabled()) { + if (!askToSendContests()) { + sendContests() + } else { + LorenzUtils.clickableChat( + "§e[SkyHanni] §2Click here to submit this years farming contests, thank you for helping everyone out!", + "shsendcontests" + ) + } + } + } update() saveConfig() } @@ -141,15 +177,53 @@ class GardenNextJacobContest { private fun saveConfig() { val map = SkyHanniMod.feature.storage.gardenJacobFarmingContestTimes map.clear() + + val currentYear = SkyBlockTime.now().year for (contest in contests.values) { + val contestYear = (SkyBlockTime.fromInstant(Instant.ofEpochMilli(contest.endTime))).year + // Ensure all stored contests are really from the current year + if (contestYear != currentYear) continue + map[contest.endTime] = contest.crops } } @SubscribeEvent fun onConfigLoad(event: ConfigLoadEvent) { - for ((time, crops) in SkyHanniMod.feature.storage.gardenJacobFarmingContestTimes) { - contests[time] = FarmingContest(time, crops) + val savedContests = SkyHanniMod.feature.storage.gardenJacobFarmingContestTimes + val year = savedContests.firstNotNullOfOrNull { + val endTime = it.key + + SkyBlockTime.fromInstant(Instant.ofEpochMilli(endTime)).year + } + + // Clear contests if from previous year + if (year != SkyBlockTime.now().year) { + savedContests.clear() + } else { + for ((time, crops) in savedContests) { + contests[time] = FarmingContest(time, crops) + } + } + } + + fun shareContestConfirmed(array: Array) { + if (array.size == 1) { + if (array[0] == "enable") { + config.nextJacobContestsShareAutomatically = 1 + SkyHanniMod.feature.storage.contestSendingAsked = true + LorenzUtils.chat("§e[SkyHanni] §2Enabled automatic sharing of future contests!") + } + return + } + if (contests.size == maxContestsPerYear) { + sendContests() + } + if (!SkyHanniMod.feature.storage.contestSendingAsked && config.nextJacobContestsShareAutomatically == 0) { + LorenzUtils.clickableChat( + "§e[SkyHanni] §2Click here to automatically share future contests!", + "shsendcontests enable" + ) } } @@ -157,7 +231,20 @@ class GardenNextJacobContest { private fun update() { nextContestCrops.clear() - display = drawDisplay() + + if (nextContestsAvailableAt == -1L) { + val currentDate = SkyBlockTime.now() + if (currentDate.month <= 1 && currentDate.day <= 1) { + nextContestsAvailableAt = SkyBlockTime(SkyBlockTime.now().year + 1, 1, 1).toMillis() + } + } + + display = if (isFetchingContests) { + listOf("§cFetching this years jacob contests...") + } else { + fetchContestsIfAble() // Will only run when needed/enabled + drawDisplay() + } } private fun drawDisplay(): List { @@ -173,22 +260,26 @@ class GardenNextJacobContest { } if (contests.isEmpty()) { - return emptyList() + list.add("§cOpen calendar to read jacob contest times!") + return list } val nextContest = contests.filter { it.value.endTime > System.currentTimeMillis() }.toSortedMap() .firstNotNullOfOrNull { it.value } - if (nextContest == null) { - if (contests.size == maxContestsPerYear) { - list.add("§cNew SkyBlock Year! Open calendar again!") - } else { - list.add("§cOpen calendar to read jacob contest times!") - } - return list + // Show next contest + if (nextContest != null) return drawNextContest(nextContest, list) + + if (contests.size == maxContestsPerYear) { + list.add("§cNew SkyBlock Year! Open calendar again!") + } else { + list.add("§cOpen calendar to read jacob contest times!") } - return drawNextContest(nextContest, list) + fetchedFromElite = false + contests.clear() + + return list } private fun drawNextContest( @@ -301,10 +392,105 @@ class GardenNextJacobContest { private fun isEnabled() = LorenzUtils.inSkyBlock && config.nextJacobContestDisplay && (GardenAPI.inGarden() || config.nextJacobContestEverywhere) - companion object { - private val config get() = SkyHanniMod.feature.garden - private val nextContestCrops = mutableListOf() + private fun isFetchEnabled() = isEnabled() && config.nextJacobContestsFetchAutomatically + private fun isSendEnabled() = isFetchEnabled() && config.nextJacobContestsShareAutomatically != 2 // 2 = Disabled + private fun askToSendContests() = + config.nextJacobContestsShareAutomatically == 0 // 0 = Ask, 1 = Send (Only call if isSendEnabled()) + + private fun fetchContestsIfAble() { + if (isFetchingContests || contests.size == maxContestsPerYear || !isFetchEnabled()) return + // Allows retries every 10 minutes when it's after 1 day into the new year + val currentMills = System.currentTimeMillis() + if (lastFetchAttempted + 600_000 > currentMills || currentMills < nextContestsAvailableAt) return + + isFetchingContests = true + + SkyHanniMod.coroutineScope.launch { + fetchUpcomingContests() + lastFetchAttempted = System.currentTimeMillis() + isFetchingContests = false + } + } + + private suspend fun fetchUpcomingContests() { + try { + val url = "https://api.elitebot.dev/contests/at/now" + val result = withContext(Dispatchers.IO) { APIUtil.getJSONResponse(url) }.asJsonObject + + val newContests = mutableMapOf() + + val complete = result["complete"].asBoolean + if (complete) { + for (entry in result["contests"].asJsonObject.entrySet()) { + var timestamp = entry.key.toLongOrNull() ?: continue + timestamp *= 1_000 // Seconds to milliseconds + + val crops = entry.value.asJsonArray.map { + CropType.getByName(it.asString) + } + + if (crops.size != 3) continue + + newContests[timestamp + contestDuration] = FarmingContest(timestamp + contestDuration, crops) + } + } else { + LorenzUtils.chat("§e[SkyHanni] This years contests aren't available to fetch automatically yet, please load them from your calender or wait 10 minutes!") + } + + if (newContests.count() == maxContestsPerYear) { + LorenzUtils.chat("§e[SkyHanni] Successfully loaded this year's contests from elitebot.dev automatically!") + + contests = newContests + fetchedFromElite = true + nextContestsAvailableAt = SkyBlockTime(SkyBlockTime.now().year + 1, 1, 2).toMillis() + loadedContestsYear = SkyBlockTime.now().year + + saveConfig() + } + } catch (e: Exception) { + e.printStackTrace() + LorenzUtils.error("[SkyHanni] Failed to fetch upcoming contests. Please report this error if it continues to occur.") + } + } + + private fun sendContests() { + if (isSendingContests || contests.size != maxContestsPerYear) return - fun isNextCrop(cropName: CropType) = nextContestCrops.contains(cropName) && config.nextJacobContestOtherGuis + isSendingContests = true + + SkyHanniMod.coroutineScope.launch { + submitContestsToElite() + isSendingContests = false + } } + + private suspend fun submitContestsToElite() = try { + val formatted = mutableMapOf>() + + for ((endTime, contest) in contests) { + formatted[endTime / 1000] = contest.crops.map { + it.cropName + } + } + + val url = "https://api.elitebot.dev/contests/at/now" + val body = Gson().toJson(formatted) + + val result = withContext(Dispatchers.IO) { APIUtil.postJSONIsSuccessful(url, body) } + + if (result) { + LorenzUtils.chat("§e[SkyHanni] Successfully submitted this years upcoming contests, thank you for helping everyone out!") + } else { + LorenzUtils.error("[SkyHanni] Something went wrong submitting upcoming contests!") + } + } catch (e: Exception) { + e.printStackTrace() + LorenzUtils.error("[SkyHanni] Failed to submit upcoming contests. Please report this error if it continues to occur.") + null + } + + private val config get() = SkyHanniMod.feature.garden + private val nextContestCrops = mutableListOf() + + fun isNextCrop(cropName: CropType) = nextContestCrops.contains(cropName) && config.nextJacobContestOtherGuis } \ No newline at end of file diff --git a/src/main/java/at/hannibal2/skyhanni/utils/APIUtil.kt b/src/main/java/at/hannibal2/skyhanni/utils/APIUtil.kt index 04bd67c18..917243b47 100644 --- a/src/main/java/at/hannibal2/skyhanni/utils/APIUtil.kt +++ b/src/main/java/at/hannibal2/skyhanni/utils/APIUtil.kt @@ -7,6 +7,9 @@ import com.google.gson.JsonParser import com.google.gson.JsonSyntaxException import org.apache.http.client.config.RequestConfig import org.apache.http.client.methods.HttpGet +import org.apache.http.client.methods.HttpPost +import org.apache.http.entity.ContentType +import org.apache.http.entity.StringEntity import org.apache.http.impl.client.HttpClientBuilder import org.apache.http.impl.client.HttpClients import org.apache.http.message.BasicHeader @@ -83,6 +86,38 @@ object APIUtil { return JsonObject() } + fun postJSONIsSuccessful(urlString: String, body: String, silentError: Boolean = false): Boolean { + val client = builder.build() + try { + val method = HttpPost(urlString) + method.entity = StringEntity(body, ContentType.APPLICATION_JSON) + + client.execute(method).use { response -> + val status = response.statusLine + + if (status.statusCode >= 200 || status.statusCode < 300) { + return true + } + + println("POST request to '$urlString' returned status ${status.statusCode}") + LorenzUtils.error("SkyHanni ran into an error whilst sending data. Status: ${status.statusCode}") + + return false + } + } catch (throwable: Throwable) { + if (silentError) { + throw throwable + } else { + throwable.printStackTrace() + LorenzUtils.error("SkyHanni ran into an ${throwable::class.simpleName ?: "error"} whilst sending a resource. See logs for more details.") + } + } finally { + client.close() + } + + return false + } + fun readFile(file: File): BufferedReader { return BufferedReader(InputStreamReader(FileInputStream(file), StandardCharsets.UTF_8)) } -- cgit