aboutsummaryrefslogtreecommitdiff
path: root/src/main/java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java')
-rw-r--r--src/main/java/at/hannibal2/skyhanni/SkyHanniMod.kt2
-rw-r--r--src/main/java/at/hannibal2/skyhanni/config/Storage.java3
-rw-r--r--src/main/java/at/hannibal2/skyhanni/config/commands/Commands.kt3
-rw-r--r--src/main/java/at/hannibal2/skyhanni/config/features/GardenConfig.java12
-rw-r--r--src/main/java/at/hannibal2/skyhanni/features/garden/GardenNextJacobContest.kt270
-rw-r--r--src/main/java/at/hannibal2/skyhanni/utils/APIUtil.kt35
6 files changed, 281 insertions, 44 deletions
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
@@ -24,6 +24,9 @@ public class Storage {
public Map<Long, List<CropType>> gardenJacobFarmingContestTimes = new HashMap<>();
@Expose
+ public Boolean contestSendingAsked = false;
+
+ @Expose
public Map<UUID, PlayerSpecific> players = new HashMap<>();
public static class PlayerSpecific {
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<String>) {
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
@@ -938,6 +938,18 @@ public class GardenConfig {
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
@ConfigAccordionId(id = 14)
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<Any>()
private var simpleDisplay = emptyList<String>()
private var contests = mutableMapOf<Long, FarmingContest>()
@@ -35,9 +39,16 @@ class GardenNextJacobContest {
private val patternMonth = "(?<month>.*), Year (?<year>.*)".toPattern()
private val patternCrop = "§e○ §7(?<crop>.*)".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<ItemStack>, 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<CropType>()
- 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<CropType>()
+ 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<String>) {
+ 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<Any> {
@@ -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<CropType>()
+ 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<Long, FarmingContest>()
+
+ 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<Long, List<String>>()
+
+ 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<CropType>()
+
+ 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))
}