package at.hannibal2.skyhanni.api import at.hannibal2.skyhanni.data.ProfileStorageData import at.hannibal2.skyhanni.data.jsonobjects.repo.neu.NeuSkillLevelJson import at.hannibal2.skyhanni.events.ActionBarUpdateEvent import at.hannibal2.skyhanni.events.DebugDataCollectEvent import at.hannibal2.skyhanni.events.InventoryFullyOpenedEvent import at.hannibal2.skyhanni.events.NeuRepositoryReloadEvent import at.hannibal2.skyhanni.events.SecondPassedEvent import at.hannibal2.skyhanni.events.SkillOverflowLevelupEvent import at.hannibal2.skyhanni.features.skillprogress.SkillProgress import at.hannibal2.skyhanni.features.skillprogress.SkillType import at.hannibal2.skyhanni.features.skillprogress.SkillUtil.SPACE_SPLITTER import at.hannibal2.skyhanni.features.skillprogress.SkillUtil.XP_NEEDED_FOR_60 import at.hannibal2.skyhanni.features.skillprogress.SkillUtil.calculateLevelXp import at.hannibal2.skyhanni.features.skillprogress.SkillUtil.calculateOverFlow import at.hannibal2.skyhanni.features.skillprogress.SkillUtil.getLevel import at.hannibal2.skyhanni.features.skillprogress.SkillUtil.getLevelExact import at.hannibal2.skyhanni.features.skillprogress.SkillUtil.getSkillInfo import at.hannibal2.skyhanni.features.skillprogress.SkillUtil.xpRequiredForLevel import at.hannibal2.skyhanni.utils.ChatUtils import at.hannibal2.skyhanni.utils.ItemUtils.cleanName import at.hannibal2.skyhanni.utils.ItemUtils.getLore import at.hannibal2.skyhanni.utils.NumberUtil.addSeparators import at.hannibal2.skyhanni.utils.NumberUtil.formatDouble import at.hannibal2.skyhanni.utils.NumberUtil.formatLong import at.hannibal2.skyhanni.utils.NumberUtil.formatLongOrUserError import at.hannibal2.skyhanni.utils.NumberUtil.romanToDecimalIfNecessary import at.hannibal2.skyhanni.utils.SimpleTimeMark import at.hannibal2.skyhanni.utils.StringUtils.removeColor import at.hannibal2.skyhanni.utils.TabListData import at.hannibal2.skyhanni.utils.repopatterns.RepoPattern import com.google.gson.annotations.Expose import net.minecraft.command.CommandBase import net.minecraftforge.fml.common.eventhandler.SubscribeEvent import java.util.LinkedList import java.util.regex.Matcher import kotlin.time.Duration.Companion.seconds object SkillAPI { private val patternGroup = RepoPattern.group("api.skilldisplay") private val skillPercentPattern by patternGroup.pattern( "skill.percent", "\\+(?[\\d.,]+) (?.+) \\((?[\\d.]+)%\\)" ) private val skillPattern by patternGroup.pattern( "skill", "\\+(?[\\d.,]+) (?\\w+) \\((?[\\d.,]+)/(?[\\d.,]+)\\)" ) private val skillMultiplierPattern by patternGroup.pattern( "skill.multiplier", "\\+(?[\\d.,]+) (?.+) \\((?[\\d.,]+)/(?[\\d,.]+[kmb])\\)" ) private val skillTabPattern by patternGroup.pattern( "skill.tab", "(?:§e§lSkills: §r§a)?\\s*(?\\w+) (?\\d+): §r§.(?.+)%$" ) private val maxSkillTabPattern by patternGroup.pattern( "skill.tab.max", "(?:§e§lSkills: §r§a)?\\s*(?\\w+) (?\\d+): §r§c§lMAX\$" ) var skillXPInfoMap = mutableMapOf() var oldSkillInfoMap = mutableMapOf() val storage get() = ProfileStorageData.profileSpecific?.skillData var exactLevelingMap = mapOf() var levelingMap = mapOf() var levelArray = listOf() var activeSkill: SkillType? = null // TODO Use a map maxSkillLevel and move it into the repo val excludedSkills = listOf( SkillType.FORAGING, SkillType.FISHING, SkillType.ALCHEMY, SkillType.CARPENTRY ) var showDisplay = false var lastUpdate = SimpleTimeMark.farPast() @SubscribeEvent fun onSecondPassed(event: SecondPassedEvent) { val activeSkill = activeSkill ?: return val info = skillXPInfoMap[activeSkill] ?: return if (!info.sessionTimerActive) return val time = when (activeSkill) { SkillType.FARMING -> SkillProgress.etaConfig.farmingPauseTime SkillType.MINING -> SkillProgress.etaConfig.miningPauseTime SkillType.COMBAT -> SkillProgress.etaConfig.combatPauseTime SkillType.FORAGING -> SkillProgress.etaConfig.foragingPauseTime SkillType.FISHING -> SkillProgress.etaConfig.fishingPauseTime else -> 0 } if (info.lastUpdate.passedSince() > time.seconds) { info.sessionTimerActive = false } if (info.sessionTimerActive) { info.timeActive++ } } @SubscribeEvent fun onActionBarUpdate(event: ActionBarUpdateEvent) { val actionBar = event.actionBar.removeColor() val components = SPACE_SPLITTER.splitToList(actionBar) for (component in components) { val matcher = listOf(skillPattern, skillPercentPattern, skillMultiplierPattern) .firstOrNull { it.matcher(component).matches() } ?.matcher(component) if (matcher?.matches() == true) { val skillName = matcher.group("skillName") val skillType = SkillType.getByNameOrNull(skillName) ?: return val skillInfo = storage?.get(skillType) ?: SkillInfo() val skillXp = skillXPInfoMap[skillType] ?: SkillXPInfo() activeSkill = skillType when (matcher.pattern()) { skillPattern -> handleSkillPattern(matcher, skillType, skillInfo) skillPercentPattern -> handleSkillPatternPercent(matcher, skillType) skillMultiplierPattern -> handleSkillPatternMultiplier(matcher, skillType, skillInfo) } showDisplay = true lastUpdate = SimpleTimeMark.now() skillXp.lastUpdate = SimpleTimeMark.now() skillXp.sessionTimerActive = true SkillProgress.updateDisplay() SkillProgress.hideInActionBar = listOf(component) return } } } @SubscribeEvent fun onNEURepoReload(event: NeuRepositoryReloadEvent) { levelArray = event.readConstant("leveling").levelingXp levelingMap = levelArray.withIndex().associate { (index, xp) -> index to xp } exactLevelingMap = levelArray.withIndex().associate { (index, xp) -> xp to index } } @SubscribeEvent fun onInventoryOpen(event: InventoryFullyOpenedEvent) { val inventoryName = event.inventoryName for (stack in event.inventoryItems.values) { val lore = stack.getLore() if (inventoryName == "Your Skills" && lore.any { it.contains("Click to view!") || it.contains("Not unlocked!") } ) { val cleanName = stack.cleanName() val split = cleanName.split(" ") val skillName = split.first() val skill = SkillType.getByNameOrNull(skillName) ?: continue val skillLevel = if (split.size > 1) split.last().romanToDecimalIfNecessary() else 0 val skillInfo = storage?.getOrPut(skill) { SkillInfo() } for ((lineIndex, line) in lore.withIndex()) { val cleanLine = line.removeColor() if (!cleanLine.startsWith(" ")) continue val previousLine = stack.getLore()[lineIndex - 1] val progress = cleanLine.substring(cleanLine.lastIndexOf(' ') + 1) if (previousLine == "§7§8Max Skill level reached!") { var totalXp = progress.formatLong() val minus = if (skillLevel == 50) 4_000_000 else if (skillLevel == 60) 7_000_000 else 0 totalXp -= minus val (overflowLevel, overflowCurrent, overflowNeeded, overflowTotal) = getSkillInfo( skillLevel, totalXp, 0L, totalXp ) skillInfo?.apply { this.overflowLevel = overflowLevel this.overflowCurrentXp = overflowCurrent this.overflowCurrentXpMax = overflowNeeded this.overflowTotalXp = overflowTotal this.totalXp = totalXp this.level = skillLevel this.currentXp = totalXp this.currentXpMax = 0L } } else { val splitProgress = progress.split("/") val currentXp = splitProgress.first().formatLong() val neededXp = splitProgress.last().formatLong() val levelXp = calculateLevelXp(skillLevel - 1).toLong() skillInfo?.apply { this.currentXp = currentXp this.level = skillLevel this.currentXpMax = neededXp this.totalXp = levelXp + currentXp this.overflowCurrentXp = currentXp this.overflowLevel = skillLevel this.overflowCurrentXpMax = neededXp this.overflowTotalXp = levelXp + currentXp } } } } } } @SubscribeEvent fun onDebugDataCollectCurrent(event: DebugDataCollectEvent) { event.title("Current Skill") val storage = storage if (storage == null) { event.addIrrelevant("SkillMap is empty") return } val skillType = activeSkill if (skillType == null) { event.addIrrelevant("activeSkill is null") return } event.addData { storage[skillType]?.let { skillInfo -> addDebug(skillType, skillInfo) } } } @SubscribeEvent fun onDebugDataCollectAll(event: DebugDataCollectEvent) { event.title("All Skills") val storage = storage if (storage == null) { event.addIrrelevant("SkillMap is empty") return } event.addIrrelevant { for ((skillType, skillInfo) in storage) { addDebug(skillType, skillInfo) } } } private fun MutableList.addDebug(skillType: SkillType, skillInfo: SkillInfo) { add("Name: $skillType") add("- Level: ${skillInfo.level}") add("- CurrentXp: ${skillInfo.currentXp}") add("- CurrentXpMax: ${skillInfo.currentXpMax}") add("- TotalXp: ${skillInfo.totalXp}") add("- OverflowLevel: ${skillInfo.overflowLevel}") add("- OverflowCurrentXp: ${skillInfo.overflowCurrentXp}") add("- OverflowCurrentXpMax: ${skillInfo.overflowCurrentXpMax}") add("- OverflowTotalXp: ${skillInfo.overflowTotalXp}") add("- CustomGoalLevel: ${skillInfo.customGoalLevel}\n") } private fun handleSkillPattern(matcher: Matcher, skillType: SkillType, skillInfo: SkillInfo) { val currentXp = matcher.group("current").formatLong() val maxXp = matcher.group("needed").formatLong() val level = getLevelExact(maxXp) val (levelOverflow, currentOverflow, currentMaxOverflow, totalOverflow) = getSkillInfo( level, currentXp, maxXp, currentXp ) if (skillInfo.overflowLevel > 60 && levelOverflow == skillInfo.overflowLevel + 1) SkillOverflowLevelupEvent(skillType, skillInfo.overflowLevel, levelOverflow).postAndCatch() skillInfo.apply { this.level = level this.currentXp = currentXp this.currentXpMax = maxXp this.totalXp = currentXp this.overflowLevel = levelOverflow this.overflowCurrentXp = currentOverflow this.overflowCurrentXpMax = currentMaxOverflow this.overflowTotalXp = totalOverflow this.lastGain = matcher.group("gained") } storage?.set(skillType, skillInfo) } private fun handleSkillPatternPercent(matcher: Matcher, skillType: SkillType) { var tablistLevel = 0 for (line in TabListData.getTabList()) { var levelMatcher = skillTabPattern.matcher(line) if (levelMatcher.matches()) { val type = levelMatcher.group("type") if (type != skillType.displayName) continue tablistLevel = levelMatcher.group("level").toInt() if (levelMatcher.group("type").lowercase() != activeSkill?.lowercaseName) tablistLevel = 0 } else { levelMatcher = maxSkillTabPattern.matcher(line) if (levelMatcher.matches()) { tablistLevel = levelMatcher.group("level").toInt() if (levelMatcher.group("type").lowercase() != activeSkill?.lowercaseName) tablistLevel = 0 } } } val existingLevel = getSkillInfo(skillType) ?: SkillInfo() val xpPercentage = matcher.group("progress").formatDouble() val levelXp = calculateLevelXp(existingLevel.level - 1) val nextLevelDiff = levelArray.getOrNull(tablistLevel)?.toDouble() ?: 7_600_000.0 val nextLevelProgress = nextLevelDiff * xpPercentage / 100 val totalXp = levelXp + nextLevelProgress val (_, currentOverflow, currentMaxOverflow, totalOverflow) = getSkillInfo( tablistLevel, nextLevelProgress.toLong(), nextLevelDiff.toLong(), totalXp.toLong() ) existingLevel.apply { this.totalXp = totalXp.toLong() this.currentXp = nextLevelProgress.toLong() this.currentXpMax = nextLevelDiff.toLong() this.level = tablistLevel this.overflowTotalXp = totalOverflow this.overflowCurrentXp = currentOverflow this.overflowCurrentXpMax = currentMaxOverflow this.overflowLevel = tablistLevel this.lastGain = matcher.group("gained") } storage?.set(skillType, existingLevel) } private fun handleSkillPatternMultiplier(matcher: Matcher, skillType: SkillType, skillInfo: SkillInfo) { val currentXp = matcher.group("current").formatLong() val maxXp = matcher.group("needed").formatLong() val level = getLevelExact(maxXp) val levelXp = calculateLevelXp(level - 1).toLong() + currentXp val (currentLevel, currentOverflow, currentMaxOverflow, totalOverflow) = getSkillInfo( level, currentXp, maxXp, levelXp ) skillInfo.apply { this.overflowCurrentXp = currentOverflow this.overflowCurrentXpMax = currentMaxOverflow this.overflowTotalXp = totalOverflow this.overflowLevel = currentLevel this.currentXp = currentXp this.currentXpMax = maxXp this.totalXp = levelXp this.level = level this.lastGain = matcher.group("gained") } storage?.set(skillType, skillInfo) } fun onCommand(it: Array) { if (it.isEmpty()) { commandHelp() return } val first = it.first() if (it.size == 1) { when (first) { "goal" -> { ChatUtils.chat("§bSkill Custom Goal Level") val map = storage?.filter { it.value.customGoalLevel != 0 } ?: return if (map.isEmpty()) { ChatUtils.chat("§cYou haven't set any custom goals yet!") } map.forEach { (skill, data) -> ChatUtils.chat("§e${skill.displayName}: §b${data.customGoalLevel}") } return } } } if (it.size == 2) { val second = it[1] when (first) { "levelwithxp" -> { val xp = second.formatLongOrUserError() ?: return if (xp <= XP_NEEDED_FOR_60) { val level = getLevel(xp) ChatUtils.chat("With §b${xp.addSeparators()} §eXP you would be level §b$level") } else { val (overflowLevel, current, needed, _) = calculateOverFlow((xp) - XP_NEEDED_FOR_60) ChatUtils.chat( "With §b${xp.addSeparators()} §eXP you would be level §b$overflowLevel " + "§ewith progress (§b${current.addSeparators()}§e/§b${needed.addSeparators()}§e) XP" ) } return } "xpforlevel" -> { val level = second.toIntOrNull() if (level == null) { ChatUtils.userError("Not a valid number: '$second'") return } if (level <= 60) { val neededXp = levelingMap.filter { it.key < level }.values.sum().toLong() ChatUtils.chat("You need §b${neededXp.addSeparators()} §eXP to be level §b${level.toDouble()}") } else { val base = levelingMap.values.sum().toLong() val neededXP = xpRequiredForLevel(level.toDouble()) ChatUtils.chat("You need §b${neededXP.addSeparators()} §eXP to be level §b${level.toDouble()}") } return } "goal" -> { val rawSkill = it[1].lowercase() val skillType = SkillType.getByNameOrNull(rawSkill) if (skillType == null) { ChatUtils.userError("Unknown Skill type: $rawSkill") return } val skill = storage?.get(skillType) ?: return skill.customGoalLevel = 0 ChatUtils.chat("Custom goal level for §b${skillType.displayName} §ereset") } } } if (it.size == 3) { when (first) { "goal" -> { val rawSkill = it[1].lowercase() val skillType = SkillType.getByNameOrNull(rawSkill) if (skillType == null) { ChatUtils.userError("Unknown Skill type: $rawSkill") return } val rawLevel = it[2] val targetLevel = rawLevel.toIntOrNull() if (targetLevel == null) { ChatUtils.userError("$rawLevel is not a valid number.") return } val skill = storage?.get(skillType) ?: return if (targetLevel <= skill.overflowLevel) { ChatUtils.userError("Custom goal level ($targetLevel) must be greater than your current level (${skill.overflowLevel}).") return } skill.customGoalLevel = targetLevel ChatUtils.chat("Custom goal level for §b${skillType.displayName} §eset to §b$targetLevel") return } } } commandHelp() } fun onComplete(strings: Array): List { return when (strings.size) { 1 -> listOf("levelwithxp", "xpforlevel", "goal") 2 -> if (strings[0].lowercase() == "goal") CommandBase.getListOfStringsMatchingLastWord( strings, SkillType.entries.map { it.displayName }) else listOf() else -> listOf() } } private fun commandHelp() { ChatUtils.chat( listOf( "§6/shskills levelwithxp - §bGet a level with the given current XP.", "§6/shskills xpforlevel - §bGet how much XP you need for a desired level.", "§6/shskills goal - §bView your current goal", "§6/shskills goal - §bDefine your goal for ", "", ).joinToString("\n"), prefix = false ) } data class SkillInfo( @Expose var level: Int = 0, @Expose var totalXp: Long = 0, @Expose var currentXp: Long = 0, @Expose var currentXpMax: Long = 0, @Expose var overflowLevel: Int = 0, @Expose var overflowCurrentXp: Long = 0, @Expose var overflowTotalXp: Long = 0, @Expose var overflowCurrentXpMax: Long = 0, @Expose var lastGain: String = "", @Expose var customGoalLevel: Int = 0, ) data class SkillXPInfo( var lastTotalXp: Float = 0f, var xpGainQueue: LinkedList = LinkedList(), var xpGainHour: Float = 0f, var xpGainLast: Float = 0f, var timer: Int = 3, var sessionTimerActive: Boolean = false, var isActive: Boolean = false, var lastUpdate: SimpleTimeMark = SimpleTimeMark.farPast(), var timeActive: Long = 0L, ) }