diff --git a/src/main/java/at/hannibal2/skyhanni/SkyHanniMod.kt b/src/main/java/at/hannibal2/skyhanni/SkyHanniMod.kt index c6460ea79900..77d48c376f60 100644 --- a/src/main/java/at/hannibal2/skyhanni/SkyHanniMod.kt +++ b/src/main/java/at/hannibal2/skyhanni/SkyHanniMod.kt @@ -205,6 +205,7 @@ import at.hannibal2.skyhanni.features.garden.farming.ArmorDropTracker import at.hannibal2.skyhanni.features.garden.farming.CropMoneyDisplay import at.hannibal2.skyhanni.features.garden.farming.CropSpeedMeter import at.hannibal2.skyhanni.features.garden.farming.DicerRngDropTracker +import at.hannibal2.skyhanni.features.garden.farming.FarmingCollectionDisplay import at.hannibal2.skyhanni.features.garden.farming.FarmingWeightDisplay import at.hannibal2.skyhanni.features.garden.farming.GardenBestCropTime import at.hannibal2.skyhanni.features.garden.farming.GardenBurrowingSporesNotifier @@ -418,6 +419,7 @@ import at.hannibal2.skyhanni.features.rift.everywhere.RiftTimer import at.hannibal2.skyhanni.features.rift.everywhere.motes.RiftMotesOrb import at.hannibal2.skyhanni.features.rift.everywhere.motes.ShowMotesNpcSellPrice import at.hannibal2.skyhanni.features.skillprogress.SkillProgress +import at.hannibal2.skyhanni.features.skillprogress.SkillRankDisplay import at.hannibal2.skyhanni.features.skillprogress.SkillTooltip import at.hannibal2.skyhanni.features.slayer.HideMobNames import at.hannibal2.skyhanni.features.slayer.SlayerBossSpawnSoon @@ -939,6 +941,8 @@ class SkyHanniMod { loadModule(ColdOverlay()) loadModule(QuiverDisplay()) loadModule(QuiverWarning()) + loadModule(FarmingCollectionDisplay) + loadModule(SkillRankDisplay) init() diff --git a/src/main/java/at/hannibal2/skyhanni/config/features/garden/EliteFarmingCollectionConfig.java b/src/main/java/at/hannibal2/skyhanni/config/features/garden/EliteFarmingCollectionConfig.java new file mode 100644 index 000000000000..d0deba7b50fc --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/config/features/garden/EliteFarmingCollectionConfig.java @@ -0,0 +1,82 @@ +package at.hannibal2.skyhanni.config.features.garden; + +import at.hannibal2.skyhanni.config.FeatureToggle; +import at.hannibal2.skyhanni.config.core.config.Position; +import at.hannibal2.skyhanni.features.garden.CropType; +import com.google.gson.annotations.Expose; +import io.github.notenoughupdates.moulconfig.annotations.ConfigEditorBoolean; +import io.github.notenoughupdates.moulconfig.annotations.ConfigEditorDropdown; +import io.github.notenoughupdates.moulconfig.annotations.ConfigLink; +import io.github.notenoughupdates.moulconfig.annotations.ConfigOption; +import io.github.notenoughupdates.moulconfig.observer.Property; + +public class EliteFarmingCollectionConfig { + @Expose + @ConfigOption(name = "Display", desc = "Display your farming collection on screen. " + + "The calculation and API is provided by The Elite SkyBlock farmers. " + + "See §celitebot.dev/info §7for more info.") + @ConfigEditorBoolean + @FeatureToggle + public boolean display = true; + + @Expose + @ConfigLink(owner = EliteFarmingCollectionConfig.class, field = "display") + public Position pos = new Position(10, 60, false, true); + + @Expose + @ConfigOption(name = "Show Time Until Refresh", desc = "Show the time until the leaderboard updates.") + @ConfigEditorBoolean + public boolean showTimeUntilRefresh = true; + + @Expose + @ConfigOption(name = "Estimate Collection", desc = "Estimates how many crops you have broken between leaderboard refreshes. " + + "only works in the garden") + @ConfigEditorBoolean + public boolean estimateCollected = true; + + @Expose + @ConfigOption(name = "Show Outside Garden", desc = "Show the farming collection outside of the garden.") + @ConfigEditorBoolean + public boolean showOutsideGarden = false; + + @Expose + @ConfigOption( + name = "Crop To Display", + desc = "The crop to display on the tracker. Set to automatic to display last broken crop.") + @ConfigEditorDropdown + public Property crop = Property.of(CropDisplay.AUTO); + + public enum CropDisplay { + AUTO("Automatic", null), + WHEAT("Wheat", CropType.WHEAT), + CARROT("Carrot", CropType.CARROT), + POTATO("Potato", CropType.POTATO), + NETHER_WART("Nether Wart", CropType.NETHER_WART), + PUMPKIN("Pumpkin", CropType.PUMPKIN), + MELON("Melon", CropType.MELON), + COCOA_BEANS("Cocoa Beans", CropType.COCOA_BEANS), + SUGAR_CANE("Sugar Cane", CropType.SUGAR_CANE), + CACTUS("Cactus", CropType.CACTUS), + MUSHROOM("Mushroom", CropType.MUSHROOM), + ; + + private final String name; + private final CropType crop; + + CropDisplay(String name, CropType crop) { + this.name = name; + this.crop = crop; + } + + public CropType getCrop() { + return crop; + } + + @Override + public String toString() { + return this.name; + } + } + + +} diff --git a/src/main/java/at/hannibal2/skyhanni/config/features/garden/GardenConfig.java b/src/main/java/at/hannibal2/skyhanni/config/features/garden/GardenConfig.java index f2bdf85d5fee..a051e27c5705 100644 --- a/src/main/java/at/hannibal2/skyhanni/config/features/garden/GardenConfig.java +++ b/src/main/java/at/hannibal2/skyhanni/config/features/garden/GardenConfig.java @@ -60,6 +60,11 @@ public class GardenConfig { @Accordion public EliteFarmingWeightConfig eliteFarmingWeights = new EliteFarmingWeightConfig(); + @Expose + @ConfigOption(name = "Farming Collection", desc = "") + @Accordion + public EliteFarmingCollectionConfig eliteFarmingCollection = new EliteFarmingCollectionConfig(); + @Expose @ConfigOption(name = "Dicer RNG Drop Tracker", desc = "") @Accordion diff --git a/src/main/java/at/hannibal2/skyhanni/config/features/skillprogress/EliteSkillsDisplayConfig.java b/src/main/java/at/hannibal2/skyhanni/config/features/skillprogress/EliteSkillsDisplayConfig.java new file mode 100644 index 000000000000..4e06e84afbc2 --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/config/features/skillprogress/EliteSkillsDisplayConfig.java @@ -0,0 +1,80 @@ +package at.hannibal2.skyhanni.config.features.skillprogress; + +import at.hannibal2.skyhanni.config.FeatureToggle; +import at.hannibal2.skyhanni.config.core.config.Position; +import com.google.gson.annotations.Expose; +import io.github.notenoughupdates.moulconfig.annotations.ConfigEditorBoolean; +import io.github.notenoughupdates.moulconfig.annotations.ConfigEditorDropdown; +import io.github.notenoughupdates.moulconfig.annotations.ConfigEditorSlider; +import io.github.notenoughupdates.moulconfig.annotations.ConfigLink; +import io.github.notenoughupdates.moulconfig.annotations.ConfigOption; +import io.github.notenoughupdates.moulconfig.observer.Property; + +public class EliteSkillsDisplayConfig { + @Expose + @ConfigOption(name = "Display", desc = "Display your skill ranking on screen. " + + "The calculation and API is provided by The Elite SkyBlock farmers. " + + "See §celitebot.dev/info §7for more info.") + @ConfigEditorBoolean + @FeatureToggle + public boolean display = false; + + @Expose + @ConfigLink(owner = EliteSkillsDisplayConfig.class, field = "display") + public Position pos = new Position(10, 10, false, true); + + @Expose + @ConfigOption(name = "Always Show", desc = "Always show, even when not collecting xp.") + @ConfigEditorBoolean + public boolean alwaysShow = true; + + @Expose + @ConfigOption(name = "Cooldown", desc = "How long the display will stay after you've stopped collecting xp, in seconds") + @ConfigEditorSlider(minValue = 5, maxValue = 60, minStep = 5) + public int alwaysShowTime = 30; + + @Expose + @ConfigOption(name = "Show Time Until Refresh", desc = "Show the time until the leaderboard updates.") + @ConfigEditorBoolean + public boolean showTimeUntilRefresh = true; + + @Expose + @ConfigOption( + name = "Skill To Display", + desc = "The skill to display on the tracker. Set to automatic to display last skill gained.") + @ConfigEditorDropdown + public Property skill = Property.of(EliteSkillsDisplayConfig.SkillDisplay.AUTO); + + public enum SkillDisplay { + AUTO("Automatic", null), + COMBAT("Combat", "combat"), + MINING("Mining", "mining"), + FORAGING("Foraging", "foraging"), + FISHING("Fishing", "fishing"), + ENCHANTING("Enchanting", "enchanting"), + ALCHEMY("Alchemy", "alchemy"), + TAMING("Taming", "taming"), + CARPENTRY("Carpentry", "carpentry"), + RUNECRAFTING("Runecrafting", "runecrafting"), + SOCIAL("Social", "social"), + FARMING("Farming", "farming"), + ; + + private final String name; + private final String skill; + + SkillDisplay(String name, String skill) { + this.name = name; + this.skill = skill; + } + + public String getSkill() { + return skill; + } + + @Override + public String toString() { + return this.name; + } + } +} diff --git a/src/main/java/at/hannibal2/skyhanni/config/features/skillprogress/SkillProgressConfig.java b/src/main/java/at/hannibal2/skyhanni/config/features/skillprogress/SkillProgressConfig.java index 572cf4671df4..01fa07238b29 100644 --- a/src/main/java/at/hannibal2/skyhanni/config/features/skillprogress/SkillProgressConfig.java +++ b/src/main/java/at/hannibal2/skyhanni/config/features/skillprogress/SkillProgressConfig.java @@ -4,6 +4,7 @@ import at.hannibal2.skyhanni.config.core.config.Position; import at.hannibal2.skyhanni.utils.RenderUtils; import com.google.gson.annotations.Expose; +import io.github.notenoughupdates.moulconfig.annotations.Accordion; import io.github.notenoughupdates.moulconfig.annotations.Category; import io.github.notenoughupdates.moulconfig.annotations.ConfigEditorBoolean; import io.github.notenoughupdates.moulconfig.annotations.ConfigEditorDropdown; @@ -49,6 +50,11 @@ public String toString() { } } + @Expose + @ConfigOption(name = "Elite Bot ranking display", desc = "") + @Accordion + public EliteSkillsDisplayConfig rankDisplay = new EliteSkillsDisplayConfig(); + @Expose @ConfigOption(name = "Hide In Action Bar", desc = "Hide the skill progress in the Hypixel action bar.") @ConfigEditorBoolean diff --git a/src/main/java/at/hannibal2/skyhanni/data/jsonobjects/other/EliteBotJson.kt b/src/main/java/at/hannibal2/skyhanni/data/jsonobjects/other/EliteBotJson.kt index 2f2543f5626b..1dc506047355 100644 --- a/src/main/java/at/hannibal2/skyhanni/data/jsonobjects/other/EliteBotJson.kt +++ b/src/main/java/at/hannibal2/skyhanni/data/jsonobjects/other/EliteBotJson.kt @@ -44,3 +44,13 @@ data class PestWeightData( @Expose val brackets: Map, @Expose @SerializedName("values") val pestWeights: Map> ) + +data class EliteCollectionGraphEntry( + @Expose val timestamp: Long, + @Expose val crops: Map, +) + +data class EliteSkillGraphEntry( + @Expose val timestamp: Long, + @Expose val skills: Map, +) diff --git a/src/main/java/at/hannibal2/skyhanni/features/garden/farming/FarmingCollectionDisplay.kt b/src/main/java/at/hannibal2/skyhanni/features/garden/farming/FarmingCollectionDisplay.kt new file mode 100644 index 000000000000..d25a29a089a3 --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/features/garden/farming/FarmingCollectionDisplay.kt @@ -0,0 +1,294 @@ +package at.hannibal2.skyhanni.features.garden.farming + +import at.hannibal2.skyhanni.SkyHanniMod +import at.hannibal2.skyhanni.config.ConfigManager +import at.hannibal2.skyhanni.config.features.garden.EliteFarmingCollectionConfig.CropDisplay +import at.hannibal2.skyhanni.data.ClickType +import at.hannibal2.skyhanni.data.jsonobjects.other.EliteCollectionGraphEntry +import at.hannibal2.skyhanni.data.jsonobjects.other.EliteLeaderboard +import at.hannibal2.skyhanni.events.BlockClickEvent +import at.hannibal2.skyhanni.events.ConfigLoadEvent +import at.hannibal2.skyhanni.events.GuiRenderEvent +import at.hannibal2.skyhanni.events.LorenzChatEvent +import at.hannibal2.skyhanni.events.LorenzWorldChangeEvent +import at.hannibal2.skyhanni.events.SecondPassedEvent +import at.hannibal2.skyhanni.features.garden.CropType +import at.hannibal2.skyhanni.features.garden.CropType.Companion.getCropType +import at.hannibal2.skyhanni.features.garden.GardenAPI +import at.hannibal2.skyhanni.test.command.ErrorManager +import at.hannibal2.skyhanni.utils.APIUtil +import at.hannibal2.skyhanni.utils.ChatUtils +import at.hannibal2.skyhanni.utils.CollectionUtils.addOrPut +import at.hannibal2.skyhanni.utils.ConditionalUtils.afterChange +import at.hannibal2.skyhanni.utils.LorenzUtils +import at.hannibal2.skyhanni.utils.NumberUtil.addSeparators +import at.hannibal2.skyhanni.utils.OSUtils +import at.hannibal2.skyhanni.utils.RenderUtils.renderRenderables +import at.hannibal2.skyhanni.utils.SimpleTimeMark +import at.hannibal2.skyhanni.utils.StringUtils.toDashlessUUID +import at.hannibal2.skyhanni.utils.TimeUtils.format +import at.hannibal2.skyhanni.utils.fromJson +import at.hannibal2.skyhanni.utils.renderables.Renderable +import com.google.gson.TypeAdapter +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter +import kotlinx.coroutines.launch +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent +import java.util.UUID +import kotlin.time.Duration.Companion.minutes + +object FarmingCollectionDisplay { + + private val config get() = SkyHanniMod.feature.garden.eliteFarmingCollection + + private val CHECK_DURATION = 10.minutes + + private val eliteCollectionApiGson by lazy { + ConfigManager.createBaseGsonBuilder() + .registerTypeAdapter(CropType::class.java, object : TypeAdapter() { + override fun write(out: JsonWriter, value: CropType) {} + + override fun read(reader: JsonReader): CropType { + val crop = reader.nextString() + return CropType.entries.firstOrNull { it.simpleName == crop } ?: error("No valid crop type '$crop'") + } + }.nullSafe()) + .create() + } + + private var profileID: UUID? = null + private val collectionPlacements = mutableMapOf>() + private val collectionRanks = mutableMapOf() + private var currentCollections = mutableMapOf() + private var lastFetchedCrop: CropType? = null + + private var lastBrokenCrop: CropType? = null + private var hasFetchedCollection = false + private var lastLeaderboardFetch = SimpleTimeMark.farPast() + + private var display = emptyList() + + + @SubscribeEvent + fun onRenderOverlay(event: GuiRenderEvent) { + if (GardenAPI.hideExtraGuis()) return + if (!isEnabled()) return + + + config.pos.renderRenderables(display, posLabel = "Farming Collection Display") + } + + @SubscribeEvent + fun onWorldChange(event: LorenzWorldChangeEvent) { + resetData() + } + + @SubscribeEvent + fun onConfigLoad(event: ConfigLoadEvent) { + config.crop.afterChange { + lastLeaderboardFetch = SimpleTimeMark.farPast() + } + } + + @SubscribeEvent + fun onSecondPassed(event: SecondPassedEvent) { + if (!isEnabled()) return + if (profileID == null) return + + if (!hasFetchedCollection) { + SkyHanniMod.coroutineScope.launch { + getCurrentCollection() + } + hasFetchedCollection = true + } + + if (lastLeaderboardFetch.passedSince() > CHECK_DURATION) { + lastLeaderboardFetch = SimpleTimeMark.now() + val crop = if (config.crop.get() == CropDisplay.AUTO) { + lastBrokenCrop ?: CropType.WHEAT + } else { + config.crop.get().crop + } + + SkyHanniMod.coroutineScope.launch { + collectionPlacements.clear() + collectionRanks.clear() + getRanksForCollection(crop) + } + } + + updateDisplay() + } + + @SubscribeEvent + fun onBlockClicked(event: BlockClickEvent) { + if (event.clickType == ClickType.RIGHT_CLICK) return + val crop = event.getBlockState.getCropType() ?: return + if (!collectionRanks.containsKey(crop) && lastBrokenCrop != crop) { + SkyHanniMod.coroutineScope.launch { + getRanksForCollection(crop) + } + } + lastBrokenCrop = crop + } + + @SubscribeEvent + fun onChat(event: LorenzChatEvent) { + if (event.message.startsWith("§8Profile ID: ")) { + val id = event.message.removePrefix("§8Profile ID: ") + val newID = try { + UUID.fromString(id) + } catch (_: Exception) { + null + } + if (profileID != newID) { + resetData() + profileID = newID + } + } + } + + fun addCrop(crop: CropType, amount: Int) { + if (!isEnabled()) return + if (!config.estimateCollected) return + + currentCollections.addOrPut(crop, amount.toLong()) + } + + private fun resetData() { + hasFetchedCollection = false + lastLeaderboardFetch = SimpleTimeMark.farPast() + collectionRanks.clear() + collectionPlacements.clear() + } + + private fun updateDisplay() { + if (lastFetchedCrop == null) return + if (collectionPlacements.isEmpty()) return + if (currentCollections.isEmpty()) { + display = listOf(Renderable.wrappedString("§cCheck if your Collections \nAPI is enabled!", width = 200)) + return + } + + val rank = collectionRanks[lastFetchedCrop] ?: return + val nextRank = if (rank == -1) 5000 else rank - 1 + + val placements = collectionPlacements[lastFetchedCrop] ?: return + val collection = currentCollections[lastFetchedCrop] ?: 0 + val amountToBeat = placements[nextRank] ?: 0 + + val difference = amountToBeat - collection + + val newDisplay = mutableListOf() + newDisplay.add( + Renderable.clickAndHover( + "§6§l$lastFetchedCrop: §e${collection.addSeparators()}", + listOf("§eClick to open your Farming Profile."), + onClick = { + OSUtils.openBrowser("https://elitebot.dev/@${LorenzUtils.getPlayerName()}/") + ChatUtils.chat("Opening Farming Profile of player §b${LorenzUtils.getPlayerName()}") + } + ) + ) + if (nextRank <= 0) { + newDisplay.add( + Renderable.string("§aNo players ahead of you!") + ) + } else if (difference <= 0) { + newDisplay.add( + Renderable.clickAndHover( + "§7You have passed §b#${nextRank.addSeparators()}", + listOf("§bClick to refresh."), + onClick = { + lastLeaderboardFetch = SimpleTimeMark.farPast() + hasFetchedCollection = false + ChatUtils.chat("Collection leaderboard updating...") + } + ) + ) + } else { + newDisplay.add( + Renderable.string("§e${difference.addSeparators()} §7behind §b#${nextRank.addSeparators()}") + ) + } + if (config.showTimeUntilRefresh) { + val time = CHECK_DURATION - lastLeaderboardFetch.passedSince() + val timedisplay = if (time.isNegative()) "Now" else time.format() + + newDisplay.add( + Renderable.string("§7Refreshes in: §b$timedisplay") + ) + } + display = newDisplay + } + + private fun getRanksForCollection(crop: CropType) { + if (profileID == null) return + val url = + "https://api.elitebot.dev/Leaderboard/rank/${getEliteBotLeaderboardForCrop(crop)}/${LorenzUtils.getPlayerUuid()}/${profileID!!.toDashlessUUID()}?includeUpcoming=true" + + val response = APIUtil.getJSONResponseAsElement(url) + + try { + val data = eliteCollectionApiGson.fromJson(response) + + collectionPlacements.clear() + + collectionRanks[crop] = data.rank + val placements = mutableMapOf() + var rank = data.upcomingRank + data.upcomingPlayers.forEach { + //weight is amount + placements[rank] = it.weight.toLong() + rank-- + } + collectionPlacements[crop] = placements + lastFetchedCrop = crop + + } catch (e: Exception) { + ErrorManager.logErrorWithData( + e, + "Error loading user farming collection leaderboard\n" + + "§eLoading the farming collection leaderboard data from elitebot.dev failed!\n" + + "§eYou can re-enter the garden to try to fix the problem.\n" + + "§cIf this message repeats, please report it on Discord!\n", + "url" to url, + "apiResponse" to response, + ) + } + } + + private fun getCurrentCollection() { + if (profileID == null) return + val url = + "https://api.elitebot.dev/Graph/${LorenzUtils.getPlayerUuid()}/${profileID!!.toDashlessUUID()}/crops?days=1" + val response = APIUtil.getJSONResponseAsElement(url) + + try { + val data = eliteCollectionApiGson.fromJson>(response) + + data.sortBy { it.timestamp } + currentCollections = data.lastOrNull()?.crops?.toMutableMap() ?: mutableMapOf() + + } catch (e: Exception) { + ErrorManager.logErrorWithData( + e, + "Error loading user farming collection\n" + + "§eLoading the farming collection data from elitebot.dev failed!\n" + + "§eYou can re-enter the garden to try to fix the problem.\n" + + "§cIf this message repeats, please report it on Discord!\n", + "url" to url, + "apiResponse" to response, + ) + } + } + + private fun getEliteBotLeaderboardForCrop(crop: CropType) = when (crop) { + CropType.NETHER_WART -> "netherwart" + CropType.SUGAR_CANE -> "sugarcane" + else -> crop.simpleName + } + + private fun isEnabled() = + config.display && LorenzUtils.inSkyBlock && (GardenAPI.inGarden() || config.showOutsideGarden) +} diff --git a/src/main/java/at/hannibal2/skyhanni/features/garden/farming/GardenCropMilestoneDisplay.kt b/src/main/java/at/hannibal2/skyhanni/features/garden/farming/GardenCropMilestoneDisplay.kt index 72021eea6fc6..8819b70095a7 100644 --- a/src/main/java/at/hannibal2/skyhanni/features/garden/farming/GardenCropMilestoneDisplay.kt +++ b/src/main/java/at/hannibal2/skyhanni/features/garden/farming/GardenCropMilestoneDisplay.kt @@ -116,6 +116,7 @@ object GardenCropMilestoneDisplay { val old = cultivatingData[crop]!! val addedCounter = (counter - old).toInt() FarmingWeightDisplay.addCrop(crop, addedCounter) + FarmingCollectionDisplay.addCrop(crop, addedCounter) update() // Farming Simulator: There is a 25% chance for Mathematical Hoes and the Cultivating Enchantment to count twice. // 0.8 = 1 / 1.25 diff --git a/src/main/java/at/hannibal2/skyhanni/features/skillprogress/SkillRankDisplay.kt b/src/main/java/at/hannibal2/skyhanni/features/skillprogress/SkillRankDisplay.kt new file mode 100644 index 000000000000..44dafb2a6e46 --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/features/skillprogress/SkillRankDisplay.kt @@ -0,0 +1,267 @@ +package at.hannibal2.skyhanni.features.skillprogress + +import at.hannibal2.skyhanni.SkyHanniMod +import at.hannibal2.skyhanni.config.ConfigManager +import at.hannibal2.skyhanni.config.features.skillprogress.EliteSkillsDisplayConfig.SkillDisplay +import at.hannibal2.skyhanni.data.SkillExperience +import at.hannibal2.skyhanni.data.jsonobjects.other.EliteLeaderboard +import at.hannibal2.skyhanni.data.jsonobjects.other.EliteSkillGraphEntry +import at.hannibal2.skyhanni.events.ConfigLoadEvent +import at.hannibal2.skyhanni.events.GuiRenderEvent +import at.hannibal2.skyhanni.events.LorenzChatEvent +import at.hannibal2.skyhanni.events.LorenzWorldChangeEvent +import at.hannibal2.skyhanni.events.SecondPassedEvent +import at.hannibal2.skyhanni.events.SkillExpGainEvent +import at.hannibal2.skyhanni.features.garden.GardenAPI +import at.hannibal2.skyhanni.test.command.ErrorManager +import at.hannibal2.skyhanni.utils.APIUtil +import at.hannibal2.skyhanni.utils.ChatUtils +import at.hannibal2.skyhanni.utils.ConditionalUtils.afterChange +import at.hannibal2.skyhanni.utils.LorenzUtils +import at.hannibal2.skyhanni.utils.NumberUtil.addSeparators +import at.hannibal2.skyhanni.utils.OSUtils +import at.hannibal2.skyhanni.utils.RenderUtils.renderRenderables +import at.hannibal2.skyhanni.utils.SimpleTimeMark +import at.hannibal2.skyhanni.utils.StringUtils.firstLetterUppercase +import at.hannibal2.skyhanni.utils.StringUtils.toDashlessUUID +import at.hannibal2.skyhanni.utils.TimeUtils.format +import at.hannibal2.skyhanni.utils.fromJson +import at.hannibal2.skyhanni.utils.renderables.Renderable +import kotlinx.coroutines.launch +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent +import java.util.UUID +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds + +object SkillRankDisplay { + + private val config get() = SkyHanniMod.feature.skillProgress.rankDisplay + + private val CHECK_DURATION = 10.minutes + + private val eliteCollectionApiGson by lazy { + ConfigManager.createBaseGsonBuilder() + .create() + } + + private var profileID: UUID? = null + private val skillPlacements = mutableMapOf>() + private val skillRanks = mutableMapOf() + private var currentSkills = mutableMapOf() + + private var lastSkillGained: String? = null + private var lastSkillFetched: String? = null + private var hasFetchedSkills = false + private var lastLeaderboardFetch = SimpleTimeMark.farPast() + private var lastXPGained = SimpleTimeMark.farPast() + + private var display = emptyList() + + @SubscribeEvent + fun onRenderOverlay(event: GuiRenderEvent) { + if (GardenAPI.hideExtraGuis()) return + if (!isEnabled()) return + if (!config.alwaysShow && lastXPGained.passedSince() > config.alwaysShowTime.seconds) return + + config.pos.renderRenderables(display, posLabel = "Skill Rank Display") + } + + @SubscribeEvent + fun onWorldChange(event: LorenzWorldChangeEvent) { + resetData() + } + + @SubscribeEvent + fun onConfigLoad(event: ConfigLoadEvent) { + config.skill.afterChange { + lastLeaderboardFetch = SimpleTimeMark.farPast() + } + } + + @SubscribeEvent + fun onSecondPassed(event: SecondPassedEvent) { + if (!isEnabled()) return + if (profileID == null) return + + if (!hasFetchedSkills) { + SkyHanniMod.coroutineScope.launch { + getCurrentSkills() + } + hasFetchedSkills = true + } + + if (lastLeaderboardFetch.passedSince() > CHECK_DURATION) { + lastLeaderboardFetch = SimpleTimeMark.now() + val skill = if (config.skill.get() == SkillDisplay.AUTO) { + lastSkillGained ?: "carpentry" + } else { + config.skill.get().skill + } + + SkyHanniMod.coroutineScope.launch { + skillPlacements.clear() + skillRanks.clear() + getRanksForSkill(skill) + } + } + updateDisplay() + } + + @SubscribeEvent + fun onSkillGained(event: SkillExpGainEvent) { + if (!skillRanks.containsKey(event.skill) && lastSkillGained != event.skill) { + SkyHanniMod.coroutineScope.launch { + getRanksForSkill(event.skill) + } + } + lastXPGained = SimpleTimeMark.now() + lastSkillGained = event.skill + currentSkills[event.skill] = SkillExperience.getExpForSkill(event.skill) + } + + @SubscribeEvent + fun onChat(event: LorenzChatEvent) { + if (event.message.startsWith("§8Profile ID: ")) { + val id = event.message.removePrefix("§8Profile ID: ") + val newID = try { + UUID.fromString(id) + } catch (_: Exception) { + null + } + if (profileID != newID) { + resetData() + profileID = newID + } + } + } + + private fun resetData() { + hasFetchedSkills = false + lastLeaderboardFetch = SimpleTimeMark.farPast() + skillRanks.clear() + skillPlacements.clear() + } + + private fun updateDisplay() { + if (lastSkillFetched == null) return + if (skillPlacements.isEmpty()) return + if (currentSkills.isEmpty()) { + display = listOf(Renderable.wrappedString("§cCheck if your Skills \nAPI is enabled!", width = 200)) + return + } + + val rank = skillRanks[lastSkillFetched] ?: return + val nextRank = if (rank == -1) 5000 else rank - 1 + + val placements = skillPlacements[lastSkillFetched] ?: return + val skill = currentSkills[lastSkillFetched] ?: 0 + val amountToBeat = placements[nextRank] ?: 0 + + val difference = amountToBeat - skill + + val newDisplay = mutableListOf() + newDisplay.add( + Renderable.clickAndHover( + "§6§l${lastSkillFetched?.firstLetterUppercase()}: §e${skill.addSeparators()}", + listOf("§eClick to open your Elite Bot Profile."), + onClick = { + OSUtils.openBrowser("https://elitebot.dev/@${LorenzUtils.getPlayerName()}/") + ChatUtils.chat("Opening Elite Bot Profile of player §b${LorenzUtils.getPlayerName()}") + } + ) + ) + if (nextRank <= 0) { + newDisplay.add( + Renderable.string("§aNo players ahead of you!") + ) + } else if (difference <= 0) { + newDisplay.add( + Renderable.clickAndHover( + "§7You have passed §b#${nextRank.addSeparators()}", + listOf("§bClick to refresh."), + onClick = { + lastLeaderboardFetch = SimpleTimeMark.farPast() + hasFetchedSkills = false + ChatUtils.chat("Skills leaderboard updating...") + } + ) + ) + } else { + newDisplay.add( + Renderable.string("§e${difference.addSeparators()} §7behind §b#${nextRank.addSeparators()}") + ) + } + if (config.showTimeUntilRefresh) { + val time = CHECK_DURATION - lastLeaderboardFetch.passedSince() + val timedisplay = if (time.isNegative()) "Now" else time.format() + + newDisplay.add( + Renderable.string("§7Refreshes in: §b$timedisplay") + ) + } + display = newDisplay + } + + private fun getRanksForSkill(skill: String) { + if (profileID == null) return + val url = + "https://api.elitebot.dev/Leaderboard/rank/$skill/${LorenzUtils.getPlayerUuid()}/${profileID!!.toDashlessUUID()}?includeUpcoming=true" + + val response = APIUtil.getJSONResponseAsElement(url) + + try { + val data = eliteCollectionApiGson.fromJson(response) + + skillPlacements.clear() + + skillRanks[skill] = data.rank + val placements = mutableMapOf() + var rank = data.upcomingRank + data.upcomingPlayers.forEach { + //weight is amount + placements[rank] = it.weight.toLong() + rank-- + } + skillPlacements[skill] = placements + lastSkillFetched = skill + + } catch (e: Exception) { + ErrorManager.logErrorWithData( + e, + "Error loading user skill leaderboard\n" + + "§eLoading the skill leaderboard data from elitebot.dev failed!\n" + + "§eYou can switch worlds to try to fix the problem.\n" + + "§cIf this message repeats, please report it on Discord!\n", + "url" to url, + "apiResponse" to response, + ) + } + } + + private fun getCurrentSkills() { + if (profileID == null) return + val url = + "https://api.elitebot.dev/Graph/${LorenzUtils.getPlayerUuid()}/${profileID!!.toDashlessUUID()}/skills?days=1" + val response = APIUtil.getJSONResponseAsElement(url) + + try { + val data = eliteCollectionApiGson.fromJson>(response) + + data.sortBy { it.timestamp } + currentSkills = data.lastOrNull()?.skills?.toMutableMap() ?: mutableMapOf() + + } catch (e: Exception) { + ErrorManager.logErrorWithData( + e, + "Error loading user skill\n" + + "§eLoading the skill data from elitebot.dev failed!\n" + + "§eYou can switch worlds to try to fix the problem.\n" + + "§cIf this message repeats, please report it on Discord!\n", + "url" to url, + "apiResponse" to response, + ) + } + } + + private fun isEnabled() = config.display && LorenzUtils.inSkyBlock +}