diff --git a/.idea/dictionaries/default_user.xml b/.idea/dictionaries/default_user.xml index 39973fdd63d5..1f62e4df83c3 100644 --- a/.idea/dictionaries/default_user.xml +++ b/.idea/dictionaries/default_user.xml @@ -62,6 +62,7 @@ demonlord derpy despawn + despawned dicer disintegrator disintegrators @@ -71,6 +72,7 @@ dreadlord dungeoneering dwarven + eequipped egglocator einary einary's @@ -95,6 +97,7 @@ ghast glacite glowstone + goback goldor golem gratitudes @@ -230,6 +233,7 @@ sethome shcopytranslation shcropstartlocation + shelmet shlanedetection shmarkplayer shmouselock diff --git a/.live-plugins/event/plugin.kts b/.live-plugins/event/plugin.kts index d012154b881f..ff289c705a28 100644 --- a/.live-plugins/event/plugin.kts +++ b/.live-plugins/event/plugin.kts @@ -19,6 +19,7 @@ import org.jetbrains.kotlin.types.typeUtil.supertypes val skyhanniEvent = "at.hannibal2.skyhanni.api.event.SkyHanniEvent" val handleEvent = "HandleEvent" +val eventType = "eventType" registerInspection(HandleEventInspectionKotlin()) @@ -28,16 +29,33 @@ class HandleEventInspectionKotlin : AbstractKotlinInspection() { val visitor = object : KtVisitorVoid() { override fun visitNamedFunction(function: KtNamedFunction) { val hasEventAnnotation = function.annotationEntries.any { it.shortName!!.asString() == handleEvent } + + // Check if the function's parameter is a SkyHanniEvent or its subtype val isEvent = function.valueParameters.firstOrNull()?.type()?.supertypes() ?.any { it.fqName?.asString() == skyhanniEvent } ?: false + // Find the annotation entry + val annotationEntry = function.annotationEntries + .find { it.shortName!!.asString() == handleEvent } + + // Check if the annotation specifies the eventType explicitly or as a positional parameter + val hasEventType = annotationEntry?.valueArguments + ?.any { argument -> + val argName = argument.getArgumentName()?.asName?.asString() + argName == eventType || argName == "eventTypes" || + // Check if it is a positional argument (first argument) + (annotationEntry.valueArguments.indexOf(argument) == 0 && + argument.getArgumentExpression()?.text != null) + } ?: false + + // Validate function annotation and parameters if (isEvent && !hasEventAnnotation && function.valueParameters.size == 1 && function.isPublic) { holder.registerProblem( function, "Event handler function should be annotated with @HandleEvent", HandleEventQuickFix() ) - } else if (!isEvent && hasEventAnnotation) { + } else if (!isEvent && !hasEventType && hasEventAnnotation) { holder.registerProblem( function, "Function should not be annotated with @HandleEvent if it does not take a SkyHanniEvent", diff --git a/annotation-processors/src/main/kotlin/at/hannibal2/skyhanni/skyhannimodule/ModuleProcessor.kt b/annotation-processors/src/main/kotlin/at/hannibal2/skyhanni/skyhannimodule/ModuleProcessor.kt index 73253a9a0788..0b226acc9f04 100644 --- a/annotation-processors/src/main/kotlin/at/hannibal2/skyhanni/skyhannimodule/ModuleProcessor.kt +++ b/annotation-processors/src/main/kotlin/at/hannibal2/skyhanni/skyhannimodule/ModuleProcessor.kt @@ -68,8 +68,13 @@ class ModuleProcessor(private val codeGenerator: CodeGenerator, private val logg } if (function.annotations.any { it.shortName.asString() == "HandleEvent" }) { - val firstParameter = function.parameters.firstOrNull()?.type?.resolve()!! - if (!skyHanniEvent!!.isAssignableFrom(firstParameter)) { + val firstParameter = function.parameters.firstOrNull()?.type?.resolve() + val handleEventAnnotation = function.annotations.find { it.shortName.asString() == "HandleEvent" } + val eventType = handleEventAnnotation?.arguments?.find { it.name?.asString() == "eventType" }?.value + val isFirstParameterProblem = firstParameter == null && eventType == null + val notAssignable = firstParameter != null && !skyHanniEvent!!.isAssignableFrom(firstParameter) + + if (isFirstParameterProblem || notAssignable) { warnings.add("Function in $className must have an event assignable from $skyHanniEvent because it is annotated with @HandleEvent") } } diff --git a/src/main/java/at/hannibal2/skyhanni/api/CurrentPetApi.kt b/src/main/java/at/hannibal2/skyhanni/api/CurrentPetApi.kt new file mode 100644 index 000000000000..df252dc3e001 --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/api/CurrentPetApi.kt @@ -0,0 +1,417 @@ +package at.hannibal2.skyhanni.api + +import at.hannibal2.skyhanni.SkyHanniMod +import at.hannibal2.skyhanni.api.event.HandleEvent +import at.hannibal2.skyhanni.data.PetData +import at.hannibal2.skyhanni.data.PetData.Companion.parsePetAsItem +import at.hannibal2.skyhanni.data.PetData.Companion.parsePetData +import at.hannibal2.skyhanni.data.PetData.Companion.parsePetDataLists +import at.hannibal2.skyhanni.data.PetData.Companion.petNameToInternalName +import at.hannibal2.skyhanni.data.PetDataStorage +import at.hannibal2.skyhanni.data.ProfileStorageData +import at.hannibal2.skyhanni.data.model.TabWidget +import at.hannibal2.skyhanni.events.DebugDataCollectEvent +import at.hannibal2.skyhanni.events.GuiContainerEvent +import at.hannibal2.skyhanni.events.InventoryCloseEvent +import at.hannibal2.skyhanni.events.InventoryFullyOpenedEvent +import at.hannibal2.skyhanni.events.WidgetUpdateEvent +import at.hannibal2.skyhanni.events.chat.SkyHanniChatEvent +import at.hannibal2.skyhanni.events.skyblock.PetChangeEvent +import at.hannibal2.skyhanni.skyhannimodule.SkyHanniModule +import at.hannibal2.skyhanni.test.command.ErrorManager +import at.hannibal2.skyhanni.utils.ChatUtils +import at.hannibal2.skyhanni.utils.ItemCategory +import at.hannibal2.skyhanni.utils.ItemUtils.getItemCategoryOrNull +import at.hannibal2.skyhanni.utils.ItemUtils.getLore +import at.hannibal2.skyhanni.utils.LorenzColor.Companion.toLorenzColor +import at.hannibal2.skyhanni.utils.LorenzRarity +import at.hannibal2.skyhanni.utils.NeuInternalName +import at.hannibal2.skyhanni.utils.NumberUtil.formatDouble +import at.hannibal2.skyhanni.utils.PetUtils.isPetMenu +import at.hannibal2.skyhanni.utils.PetUtils.levelToXp +import at.hannibal2.skyhanni.utils.PetUtils.rarityByColorGroup +import at.hannibal2.skyhanni.utils.Quad +import at.hannibal2.skyhanni.utils.RegexUtils.firstMatchGroup +import at.hannibal2.skyhanni.utils.RegexUtils.firstMatcher +import at.hannibal2.skyhanni.utils.RegexUtils.firstMatches +import at.hannibal2.skyhanni.utils.RegexUtils.groupOrNull +import at.hannibal2.skyhanni.utils.RegexUtils.hasGroup +import at.hannibal2.skyhanni.utils.RegexUtils.matchMatcher +import at.hannibal2.skyhanni.utils.RegexUtils.matches +import at.hannibal2.skyhanni.utils.StringUtils.convertToUnformatted +import at.hannibal2.skyhanni.utils.chat.Text.hover +import at.hannibal2.skyhanni.utils.repopatterns.RepoPattern + +@SkyHanniModule +object CurrentPetApi { + private val config get() = SkyHanniMod.feature.misc.pets + val patternGroup = RepoPattern.group("misc.pet") + + private var inPetMenu = false + private var lastPetLine: String? = null + + var currentPet: PetData? + get() = ProfileStorageData.profileSpecific?.currentPetData?.toPetData()?.takeIf { it.isInitialized() } + set(value) { + ProfileStorageData.profileSpecific?.currentPetData = value?.asStorage() ?: PetDataStorage() + } + + fun isCurrentPet(petInternalName: NeuInternalName): Boolean = currentPet?.petItem == petInternalName + fun isCurrentPet(petName: String): Boolean = currentPet?.cleanName?.contains(petName) ?: false + + // + /** + * REGEX-TEST: §r§7[Lvl 100] §r§dEndermite + * REGEX-TEST: §r§7[Lvl 200] §r§8[§r§6108§r§8§r§4✦§r§8] §r§6Golden Dragon + * REGEX-TEST: §r§7[Lvl 100] §r§dBlack Cat§r§d ✦ + */ + @Suppress("MaxLineLength") + private val petWidgetPattern by patternGroup.pattern( + "widget.pet", + "^ §r§7\\[Lvl (?\\d+)](?: (?:§.)+\\[(?:§.)+(?\\d+)(?:§.)+✦(?:§.)+])? §r§(?.)(?[\\w ]+)(?:§r(?§. ✦))?\$", + ) + + /** + * REGEX-TEST: §cAutopet §eequipped your §7[Lvl 100] §6Scatha§e! §a§lVIEW RULE + * REGEX-TEST: §cAutopet §eequipped your §7[Lvl 99] §6Flying Fish§e! §a§lVIEW RULE + * REGEX-TEST: §cAutopet §eequipped your §7[Lvl 100] §dBlack Cat§d ✦§e! §a§lVIEW RULE + * REGEX-TEST: §cAutopet §eequipped your §7[Lvl 100] §6Griffin§4 ✦§e! §a§lVIEW RULE + * REGEX-TEST: §cAutopet §eequipped your §7[Lvl 100] §6Elephant§e! §a§lVIEW RULE + */ + private val autopetMessagePattern by patternGroup.pattern( + "chat.autopet", + "^§cAutopet §eequipped your §7(?\\[Lvl \\d{1,3}] §.[\\w ]+)(?:§. ✦)?§e! §a§lVIEW RULE\$", + ) + + /** + * REGEX-TEST: §aYour pet is now holding §r§9Bejeweled Collar§r§a. + */ + private val petItemMessagePattern by patternGroup.pattern( + "chat.pet.item.equip", + "^§aYour pet is now holding §r(?§.[\\w -]+)§r§a\\.\$", + ) + + /** + * REGEX-TEST: §7§7Selected pet: §6Hedgehog + * REGEX-TEST: §7§7Selected pet: §6Enderman + * REGEX-TEST: §7§7Selected pet: §cNone + */ + private val inventorySelectedPetPattern by patternGroup.pattern( + "inventory.selected", + "§7§7Selected pet: §?(?.)?(?.*)" + ) + + /** + * REGEX-TEST: §7Progress to Level 91: §e0% + * REGEX-TEST: §7Progress to Level 147: §e37.1% + * REGEX-TEST: §b§lMAX LEVEL + */ + private val inventorySelectedProgressPattern by patternGroup.pattern( + "inventory.selected.progress", + "§b§lMAX LEVEL|§7Progress to Level (?\\d+): §e(?[\\d.]+)%" + ) + + /** + * REGEX-TEST: §2§l§m §f§l§m §r §e713,241.8§6/§e1.4M + * REGEX-TEST: §2§l§m §f§l§m §r §e699,742.8§6/§e1.9M + * REGEX-TEST: §f§l§m §r §e0§6/§e660 + * REGEX-TEST: §8▸ 30,358,983 XP' + */ + private val inventorySelectedXpPattern by patternGroup.pattern( + "inventory.selected.xp", + "(?:§8▸ |(?:§.§l§m *)*)(?:§r §e)?(?[\\d,.kM]+)(?:§6\\/§e)?(?[\\d,.kM]+)?" + ) + + /** + * REGEX-TEST: §r§7No pet selected + * REGEX-TEST: §r§6Washed-up Souvenir + * REGEX-TEST: §r§9Dwarf Turtle Shelmet + */ + private val widgetStringPattern by patternGroup.pattern( + "widget.string", + "^ §r(?§.[\\w -]+)\$", + ) + + /** + * REGEX-TEST: §r§b§lMAX LEVEL + * REGEX-TEST: §r§6+§r§e21,248,020.7 XP + * REGEX-TEST: §r§e15,986.6§r§6/§r§e29k XP §r§6(53.6%) + */ + @Suppress("MaxLineLength") + private val xpWidgetPattern by patternGroup.pattern( + "widget.xp", + "^ §r§.(?:§l(?MAX LEVEL)|\\+§r§e(?[\\d,.]+) XP|(?[\\d,.]+)§r§6/§r§e(?[\\d.km]+) XP §r§6\\((?[\\d.%]+)\\))$", + ) + + /** + * REGEX-TEST: §r, §aEquip: §r, §7[Lvl 99] §r, §6Flying Fish + * REGEX-TEST: §r, §aEquip: §r, §e⭐ §r, §7[Lvl 100] §r, §dBlack Cat§r, §d ✦ + * REGEX-TEST: §r, §aEquip: §r, §7[Lvl 47] §r, §5Lion + */ + private val autopetHoverPetPattern by patternGroup.pattern( + "chat.autopet.hover.pet", + "^§r, §aEquip: §r,(?: §e⭐ §r,)? §7\\[Lvl (?\\d+)] §r, §(?.)(?[\\w ]+)(?:§r, (?§. ✦))?\$", + ) + + /** + * REGEX-TEST: §r, §aHeld Item: §r, §9Mining Exp Boost§r] + * REGEX-TEST: §r, §aHeld Item: §r, §5Lucky Clover§r] + * REGEX-TEST: §r, §aHeld Item: §r, §5Fishing Exp Boost§r] + */ + private val autopetHoverPetItemPattern by patternGroup.pattern( + "chat.autopet.hover.item", + "^§r, §aHeld Item: §r, (?§.[\\w -]+)§r]\$", + ) + + /** + * REGEX-TEST: §aYou despawned your §r§6Golden Dragon§r§a! + * REGEX-TEST: §aYou despawned your §r§6Silverfish§r§5 ✦§r§a! + * REGEX-TEST: §aYou despawned your §r§6Enderman§r§a! + */ + private val chatDespawnPattern by patternGroup.pattern( + "chat.despawn", + "§aYou despawned your §r.*§r§a!", + ) + + /** + * REGEX-TEST: §aYou summoned your §r§6Silverfish§r§5 ✦§r§a! + * REGEX-TEST: §aYou summoned your §r§6Golden Dragon§r§a! + * REGEX-TEST: §aYou summoned your §r§6Enderman§r§a! + */ + private val chatSpawnPattern by patternGroup.pattern( + "chat.spawn", + "§aYou summoned your §r(?.*)§r§a!" + ) + + /** + * REGEX-TEST: §7§cClick to despawn! + */ + val petDespawnMenuPattern by patternGroup.pattern( + "menu.pet.despawn", + "§7§cClick to despawn!", + ) + // + + // + private fun updatePet(newPet: PetData?) { + val oldPet = currentPet + if (newPet == oldPet) return + if (newPet?.allButSkinEquivalent(oldPet) == true) { + // If the two pets are the same except for the skin, we want to take the one that has the skin. + // If they both have differing skins, we want to take the new one. + if (oldPet?.skinInternalName != null && newPet.skinInternalName == null) return + } + + currentPet = newPet + if (SkyHanniMod.feature.dev.debug.petEventMessages) { + ChatUtils.debug("oldPet: " + oldPet.toString().convertToUnformatted()) + ChatUtils.debug("newPet: " + newPet.toString().convertToUnformatted()) + } + PetChangeEvent(oldPet, newPet).post() + } + + private fun handlePetMessageBlock(event: SkyHanniChatEvent) { + if (!config.hideAutopet) return + val spawnMatches = chatSpawnPattern.matches(event.message) + val despawnMatches = chatDespawnPattern.matches(event.message) + val autoPetMatches = autopetMessagePattern.matches(event.message) + if (spawnMatches || despawnMatches || autoPetMatches) { + event.blockedReason = "pets" + } + } + // + + // + private fun handleWidgetPetLine(line: String): PetData? = petWidgetPattern.matchMatcher(line) { + val rarity = rarityByColorGroup(group("rarity")) + val petName = groupOrNull("name").orEmpty() + val petInternalName = petNameToInternalName(petName, rarity) + val level = groupOrNull("level")?.toInt() ?: 0 + val xp = levelToXp(level, petInternalName) ?: return null + val skinColor = groupOrNull("skin")?.substring(1)?.get(0)?.toLorenzColor() + + return PetData( + petItem = petInternalName, + heldItem = null, + cleanName = petName, + rarity = rarity, + level = level, + xp = xp, + skinSymbolColor = skinColor, + ) + } + + private fun handleWidgetStringLine(line: String): NeuInternalName? = widgetStringPattern.matchMatcher(line) { + val string = group("string") + if (string == "No pet selected") { + updatePet(null) + return null + } + return NeuInternalName.fromItemNameOrNull(string) + } + + private fun handleWidgetXPLine(line: String): Double? = xpWidgetPattern.matchMatcher(line) { + if (hasGroup("max")) return null + + group("overflow")?.formatDouble() ?: group("currentXP")?.formatDouble() + } + // + + // + private fun onAutopetMessage(event: SkyHanniChatEvent) { + val hoverMessage = event.chatComponent.hover?.siblings?.joinToString("")?.split("\n") ?: return + + val (petData, _) = parsePetData( + hoverMessage, + { readAutopetItemMessage(it) }, + { null }, // No overflow XP handling in this case + { readAutopetMessage(it) } + ) ?: return + + updatePet(petData) + } + + private fun readAutopetMessage(string: String): PetData? = autopetHoverPetPattern.matchMatcher(string) { + val level = group("level").toInt() + val rarity = rarityByColorGroup(group("rarity")) + val petName = group("pet") + val petInternalName = petNameToInternalName(petName, rarity) + + return PetData( + petItem = petInternalName, + cleanName = petName, + rarity = rarity, + level = level, + xp = levelToXp(level, petInternalName) ?: 0.0, + ) + } + + private fun readAutopetItemMessage(string: String): NeuInternalName? = autopetHoverPetItemPattern.matchMatcher(string) { + NeuInternalName.fromItemNameOrNull(group("item")) + } + // + + // + private fun extractSelectedPetData(lore: List): Quad? { + val level = inventorySelectedProgressPattern.firstMatchGroup(lore, "level")?.toInt() + val rarity = inventorySelectedPetPattern.firstMatchGroup(lore, "rarity")?.let { rarityByColorGroup(it) } + val petName = inventorySelectedPetPattern.firstMatchGroup(lore, "pet") + val petInternalName = petName?.let { + petNameToInternalName(it, rarity ?: return null) + } + + return if (level != null && rarity != null && petInternalName != null) { + Quad(level, rarity, petInternalName, petName) + } else null + } + + private fun handleSelectedPetName(lore: List): NeuInternalName? = inventorySelectedPetPattern.firstMatcher(lore) { + extractSelectedPetData(lore)?.third ?: return null + } + + private fun handleSelectedPetOverflowXp(lore: List): Double? { + // Only have overflow if `next` group is absent + if (inventorySelectedXpPattern.firstMatchGroup(lore, "next") != null) return 0.0 + val (level, _, petInternalName, _) = extractSelectedPetData(lore) ?: return null + val maxXpNeeded = levelToXp(level, petInternalName) + val currentXp = inventorySelectedXpPattern.firstMatchGroup(lore, "current")?.formatDouble() ?: 0.0 + return maxXpNeeded?.minus(currentXp) ?: 0.0 + } + + private fun handleSelectedPetData(lore: List): PetData? { + val (level, rarity, petInternalName, petName) = extractSelectedPetData(lore) ?: return null + val partialXp = inventorySelectedXpPattern.firstMatchGroup(lore, "current")?.formatDouble() ?: 0.0 + val nextExists = inventorySelectedXpPattern.firstMatchGroup(lore, "next") != null + val totalXp = partialXp + if (nextExists) (levelToXp(level, petInternalName) ?: return null) else 0.0 + return PetData( + petItem = petInternalName, + cleanName = petName, + rarity = rarity, + heldItem = null, + level = level, + xp = totalXp, + ) + } + // + + // + @HandleEvent(onlyOnSkyblock = true) + fun onWidgetUpdate(event: WidgetUpdateEvent) { + if (!event.isWidget(TabWidget.PET)) return + + val newPetLine = petWidgetPattern.firstMatches(event.lines)?.trim() ?: return + if (newPetLine == lastPetLine) return + lastPetLine = newPetLine + + val (petData, overflowXP) = parsePetData( + event.lines, + { handleWidgetStringLine(it) }, + { handleWidgetXPLine(it) }, + { handleWidgetPetLine(it) } + ) ?: return + + updatePet(petData.copy(xp = petData.xp?.plus(overflowXP))) + } + + @HandleEvent(onlyOnSkyblock = true) + fun onChat(event: SkyHanniChatEvent) { + handlePetMessageBlock(event) + if (autopetMessagePattern.matches(event.message)) { + onAutopetMessage(event) + return + } + petItemMessagePattern.matchMatcher(event.message) { + val item = NeuInternalName.fromItemNameOrNull(group("petItem")) ?: ErrorManager.skyHanniError( + "Couldn't parse pet item name.", + Pair("message", event.message), + Pair("item", group("petItem")), + ) + val newPet = currentPet?.copy(heldItem = item) ?: return + updatePet(newPet) + } + } + + @HandleEvent + fun onInventoryOpen(event: InventoryFullyOpenedEvent) { + inPetMenu = isPetMenu(event.inventoryName, event.inventoryItems) + if (!inPetMenu) return + + val lore = event.inventoryItems[4]?.getLore() ?: return + val (petData, overflowXp) = parsePetDataLists( + lore, + { handleSelectedPetName(lore) }, + { handleSelectedPetOverflowXp(lore) }, + { handleSelectedPetData(lore) } + ) ?: return + updatePet(petData.copy(xp = petData.xp?.plus(overflowXp))) + } + + @HandleEvent(InventoryCloseEvent::class) + fun onInventoryClose() { inPetMenu = false } + + @HandleEvent + fun onSlotClick(event: GuiContainerEvent.SlotClickEvent) { + if (!inPetMenu) return + if (event.clickType != GuiContainerEvent.ClickType.NORMAL) return + val item = event.item.takeIf { it?.getItemCategoryOrNull() == ItemCategory.PET } ?: return + + updatePet(parsePetAsItem(item)) + } + + @HandleEvent + fun onDebug(event: DebugDataCollectEvent) { + event.title("CurrentPetApi") + if (currentPet?.isInitialized() == false) { + event.addIrrelevant("no pet equipped") + return + } + event.addIrrelevant { + add("petName: '${currentPet?.petItem ?: ""}'") + add("petRarity: '${currentPet?.rarity?.rawName.orEmpty()}'") + add("petItem: '${currentPet?.heldItem ?: ""}'") + add("petLevel: '${currentPet?.level ?: 0}'") + add("petXP: '${currentPet?.xp ?: 0.0}'") + } + } + // +} diff --git a/src/main/java/at/hannibal2/skyhanni/api/event/EventListeners.kt b/src/main/java/at/hannibal2/skyhanni/api/event/EventListeners.kt index 22c9496e9a0b..a7fecbc916cc 100644 --- a/src/main/java/at/hannibal2/skyhanni/api/event/EventListeners.kt +++ b/src/main/java/at/hannibal2/skyhanni/api/event/EventListeners.kt @@ -24,30 +24,72 @@ class EventListeners private constructor(val name: String, private val isGeneric ) fun addListener(method: Method, instance: Any, options: HandleEvent) { - require(method.parameterCount == 1) - val generic: Class<*>? = if (isGeneric) { - ReflectionUtils.resolveUpperBoundSuperClassGenericParameter( - method.genericParameterTypes[0], - GenericSkyHanniEvent::class.java.typeParameters[0], - ) ?: error( - "Generic event handler type parameter is not present in " + - "event class hierarchy for type ${method.genericParameterTypes[0]}", + val name = buildListenerName(method) + val eventConsumer = createEventConsumer(method, instance, options) + val generic = if (isGeneric) resolveGenericType(method) else null + + listeners.add(Listener(name, eventConsumer, options, generic)) + } + + private fun buildListenerName(method: Method): String { + val paramTypesString = method.parameterTypes.joinTo( + StringBuilder(), + prefix = "(", + postfix = ")", + separator = ", ", + transform = Class<*>::getTypeName + ).toString() + + return "${method.declaringClass.name}.${method.name}$paramTypesString" + } + + private fun createEventConsumer(method: Method, instance: Any, options: HandleEvent): (Any) -> Unit { + return when (method.parameterCount) { + 0 -> createZeroParameterConsumer(method, instance, options) + 1 -> createSingleParameterConsumer(method, instance) + else -> throw IllegalArgumentException( + "Method ${method.name} must have either 0 or 1 parameters." ) + } + } + + private fun createZeroParameterConsumer(method: Method, instance: Any, options: HandleEvent): (Any) -> Unit { + if (options.eventTypes.isNotEmpty()) { + options.eventTypes.onEach { kClass -> + require(SkyHanniEvent::class.java.isAssignableFrom(kClass.java)) { + "Each event in eventTypes in @HandleEvent must extend SkyHanniEvent. Provided: $kClass" + } + } } else { - null + require(options.eventType != SkyHanniEvent::class) { + "Method ${method.name} has no parameters but no eventType was provided in the annotation." + } + require(SkyHanniEvent::class.java.isAssignableFrom(options.eventType.java)) { + "eventType in @HandleEvent must extend SkyHanniEvent. Provided: ${options.eventType.java}" + } } - val name = "${method.declaringClass.name}.${method.name}${ - method.parameterTypes.joinTo( - StringBuilder(), - prefix = "(", - postfix = ")", - separator = ", ", - transform = Class<*>::getTypeName, - ) - }" - listeners.add(Listener(name, createEventConsumer(name, instance, method), options, generic)) + + return { _: Any -> method.invoke(instance) } } + private fun createSingleParameterConsumer(method: Method, instance: Any): (Any) -> Unit { + require(SkyHanniEvent::class.java.isAssignableFrom(method.parameterTypes[0])) { + "Method ${method.name} parameter must be a subclass of SkyHanniEvent." + } + return { event -> method.invoke(instance, event) } + } + + private fun resolveGenericType(method: Method): Class<*> = + method.genericParameterTypes.getOrNull(0)?.let { genericType -> + ReflectionUtils.resolveUpperBoundSuperClassGenericParameter( + genericType, + GenericSkyHanniEvent::class.java.typeParameters[0] + ) ?: error( + "Generic event handler type parameter is not present in " + + "event class hierarchy for type $genericType" + ) + } ?: error("Method ${method.name} does not have a generic parameter type.") + /** * Creates a consumer using LambdaMetafactory, this is the most efficient way to reflectively call * a method from within code. diff --git a/src/main/java/at/hannibal2/skyhanni/api/event/HandleEvent.kt b/src/main/java/at/hannibal2/skyhanni/api/event/HandleEvent.kt index a88f7f33724c..119a4992a4fc 100644 --- a/src/main/java/at/hannibal2/skyhanni/api/event/HandleEvent.kt +++ b/src/main/java/at/hannibal2/skyhanni/api/event/HandleEvent.kt @@ -1,10 +1,23 @@ package at.hannibal2.skyhanni.api.event import at.hannibal2.skyhanni.data.IslandType +import kotlin.reflect.KClass @Retention(AnnotationRetention.RUNTIME) @Target(AnnotationTarget.FUNCTION) annotation class HandleEvent( + /** + * For cases where the event properties are themselves not needed, and solely a listener for an event fire suffices. + * To specify multiple events, use [eventTypes] instead. + */ + val eventType: KClass = SkyHanniEvent::class, + + /** + * For cases where multiple events are listened to, and properties are unnecessary. + * To specify only one event, use [eventType] instead. + */ + val eventTypes: Array> = [], + /** * If the event should only be received while on SkyBlock. */ @@ -32,7 +45,6 @@ annotation class HandleEvent( */ val receiveCancelled: Boolean = false, ) { - companion object { const val HIGHEST = -2 const val HIGH = -1 diff --git a/src/main/java/at/hannibal2/skyhanni/api/event/SkyHanniEvents.kt b/src/main/java/at/hannibal2/skyhanni/api/event/SkyHanniEvents.kt index a763f48957cc..6b229f9a694e 100644 --- a/src/main/java/at/hannibal2/skyhanni/api/event/SkyHanniEvents.kt +++ b/src/main/java/at/hannibal2/skyhanni/api/event/SkyHanniEvents.kt @@ -35,16 +35,30 @@ object SkyHanniEvents { fun isDisabledHandler(handler: String): Boolean = handler in disabledHandlers fun isDisabledInvoker(invoker: String): Boolean = invoker in disabledHandlerInvokers - @Suppress("UNCHECKED_CAST") private fun registerMethod(method: Method, instance: Any) { - if (method.parameterCount != 1) return val options = method.getAnnotation(HandleEvent::class.java) ?: return - val event = method.parameterTypes[0] - if (!SkyHanniEvent::class.java.isAssignableFrom(event)) return - listeners.getOrPut(event as Class) { EventListeners(event) } + registerSingleEventType(options, method, instance) + registerMultipleEventTypes(options, method, instance) + } + + @Suppress("UNCHECKED_CAST") + private fun registerSingleEventType(options: HandleEvent, method: Method, instance: Any) { + val eventType = method.parameterTypes.getOrNull(0) ?: options.eventType.java + if (!SkyHanniEvent::class.java.isAssignableFrom(eventType)) return + listeners.getOrPut(eventType as Class) { EventListeners(eventType) } .addListener(method, instance, options) } + @Suppress("UNCHECKED_CAST") + private fun registerMultipleEventTypes(options: HandleEvent, method: Method, instance: Any) { + options.eventTypes.map { it.java }.forEach { eventType -> + if (!SkyHanniEvent::class.java.isAssignableFrom(eventType)) return + listeners.getOrPut(eventType as Class) { EventListeners(eventType) } + .addListener(method, instance, options) + } + } + + @HandleEvent fun onRepoReload(event: RepositoryReloadEvent) { val data = event.getConstant("DisabledEvents") diff --git a/src/main/java/at/hannibal2/skyhanni/config/ConfigUpdaterMigrator.kt b/src/main/java/at/hannibal2/skyhanni/config/ConfigUpdaterMigrator.kt index e4b38806a695..29a1b768861d 100644 --- a/src/main/java/at/hannibal2/skyhanni/config/ConfigUpdaterMigrator.kt +++ b/src/main/java/at/hannibal2/skyhanni/config/ConfigUpdaterMigrator.kt @@ -12,7 +12,7 @@ import com.google.gson.JsonPrimitive object ConfigUpdaterMigrator { val logger = LorenzLogger("ConfigMigration") - const val CONFIG_VERSION = 74 + const val CONFIG_VERSION = 75 fun JsonElement.at(chain: List, init: Boolean): JsonElement? { if (chain.isEmpty()) return this if (this !is JsonObject) return null diff --git a/src/main/java/at/hannibal2/skyhanni/config/features/dev/DebugConfig.java b/src/main/java/at/hannibal2/skyhanni/config/features/dev/DebugConfig.java index b98d6403974c..25af3c8be21b 100644 --- a/src/main/java/at/hannibal2/skyhanni/config/features/dev/DebugConfig.java +++ b/src/main/java/at/hannibal2/skyhanni/config/features/dev/DebugConfig.java @@ -147,11 +147,17 @@ public class DebugConfig { @ConfigEditorBoolean public boolean oreEventMessages = false; + @Expose + @ConfigOption(name = "Pet Event Messages", desc = "Shows debug messages every time the Pet Event happens.") + @ConfigEditorBoolean + public boolean petEventMessages = false; + @Expose @ConfigOption(name = "Powder Messages", desc = "Shows debug messages every time Hotm Powder changes.") @ConfigEditorBoolean public boolean powderMessages = false; + @Expose @ConfigOption(name = "Assume Mayor", desc = "Select a mayor to assume.") @ConfigEditorDropdown diff --git a/src/main/java/at/hannibal2/skyhanni/config/features/misc/pets/PetConfig.java b/src/main/java/at/hannibal2/skyhanni/config/features/misc/pets/PetConfig.java index 2ed8129a3062..d2a5236dfd24 100644 --- a/src/main/java/at/hannibal2/skyhanni/config/features/misc/pets/PetConfig.java +++ b/src/main/java/at/hannibal2/skyhanni/config/features/misc/pets/PetConfig.java @@ -16,14 +16,9 @@ public class PetConfig { @Expose - @ConfigOption(name = "Pet Display", desc = "Show the currently active pet.") - @ConfigEditorBoolean - @FeatureToggle - public boolean display = false; - - @Expose - @ConfigLink(owner = PetConfig.class, field = "display") - public Position displayPos = new Position(-330, -15, false, true); + @ConfigOption(name = "Pet Display", desc = "") + @Accordion + public PetDisplayConfig display = new PetDisplayConfig(); @Expose @ConfigOption(name = "Pet Experience Tooltip", desc = "") diff --git a/src/main/java/at/hannibal2/skyhanni/config/features/misc/pets/PetDisplayConfig.kt b/src/main/java/at/hannibal2/skyhanni/config/features/misc/pets/PetDisplayConfig.kt new file mode 100644 index 000000000000..e11aace129bf --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/config/features/misc/pets/PetDisplayConfig.kt @@ -0,0 +1,25 @@ +package at.hannibal2.skyhanni.config.features.misc.pets + +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.ConfigLink +import io.github.notenoughupdates.moulconfig.annotations.ConfigOption + +class PetDisplayConfig { + @Expose + @ConfigOption(name = "Enabled", desc = "Show the currently active pet.") + @ConfigEditorBoolean + @FeatureToggle + var enabled: Boolean = false + + @Expose + @ConfigLink(owner = PetDisplayConfig::class, field = "enabled") + var position: Position = Position(-330, -15, false, true) + + @Expose + @ConfigOption(name = "Level Ring", desc = "Show a ring to indicate level progression.") + @ConfigEditorBoolean + var levelRing: Boolean = true +} diff --git a/src/main/java/at/hannibal2/skyhanni/config/storage/ProfileSpecificStorage.kt b/src/main/java/at/hannibal2/skyhanni/config/storage/ProfileSpecificStorage.kt index 86959f6e691a..7ae6eda9cb05 100644 --- a/src/main/java/at/hannibal2/skyhanni/config/storage/ProfileSpecificStorage.kt +++ b/src/main/java/at/hannibal2/skyhanni/config/storage/ProfileSpecificStorage.kt @@ -4,6 +4,7 @@ import at.hannibal2.skyhanni.api.HotmApi.PowderType import at.hannibal2.skyhanni.api.SkillApi import at.hannibal2.skyhanni.data.IslandType import at.hannibal2.skyhanni.data.MaxwellApi.ThaumaturgyPowerTuning +import at.hannibal2.skyhanni.data.PetDataStorage import at.hannibal2.skyhanni.data.jsonobjects.local.HotmTree import at.hannibal2.skyhanni.data.model.ComposterUpgrade import at.hannibal2.skyhanni.data.model.SkyblockStat @@ -754,7 +755,7 @@ class ProfileSpecificStorage { // data @Expose - var currentPet: String = "" + var currentPetData: PetDataStorage = PetDataStorage() @Expose var stats: MutableMap = enumMapOf() diff --git a/src/main/java/at/hannibal2/skyhanni/data/PetApi.kt b/src/main/java/at/hannibal2/skyhanni/data/PetApi.kt deleted file mode 100644 index cf2298fb017e..000000000000 --- a/src/main/java/at/hannibal2/skyhanni/data/PetApi.kt +++ /dev/null @@ -1,75 +0,0 @@ -package at.hannibal2.skyhanni.data - -import at.hannibal2.skyhanni.skyhannimodule.SkyHanniModule -import at.hannibal2.skyhanni.utils.RegexUtils.matchMatcher -import at.hannibal2.skyhanni.utils.RegexUtils.matches -import at.hannibal2.skyhanni.utils.repopatterns.RepoPattern - -@SkyHanniModule -object PetApi { - private val patternGroup = RepoPattern.group("misc.pet") - - /** - * REGEX-TEST: Pets - * REGEX-TEST: Pets (1/2) - */ - private val petMenuPattern by patternGroup.pattern( - "menu.title", - "Pets(?: \\(\\d+/\\d+\\) )?", - ) - - /** - * REGEX-TEST: §e⭐ §7[Lvl 200] §6Golden Dragon§d ✦ - * REGEX-TEST: ⭐ [Lvl 100] Black Cat ✦ - */ - private val petItemName by patternGroup.pattern( - "item.name", - "(?(?:§.)*⭐ )?(?:§.)*\\[Lvl (?\\d+)] (?.*)", - ) - - /** - * REGEX-TEST: §7[Lvl 1➡200] §6Golden Dragon - * REGEX-TEST: §7[Lvl {LVL}] §6Golden Dragon - */ - private val neuRepoPetItemName by patternGroup.pattern( - "item.name.neu.format", - "(?:§f§f)?§7\\[Lvl (?:1➡(?:100|200)|\\{LVL})] (?.*)", - ) - - private val ignoredPetStrings = listOf( - "Archer", - "Berserk", - "Mage", - "Tank", - "Healer", - "➡", - ) - - fun isPetMenu(inventoryTitle: String): Boolean = petMenuPattern.matches(inventoryTitle) - - // Contains color code + name and for older SkyHanni users maybe also the pet level - var currentPet: String? - get() = ProfileStorageData.profileSpecific?.currentPet?.takeIf { it.isNotEmpty() } - set(value) { - ProfileStorageData.profileSpecific?.currentPet = value.orEmpty() - } - - fun isCurrentPet(petName: String): Boolean = currentPet?.contains(petName) ?: false - - fun getCleanName(nameWithLevel: String): String? { - petItemName.matchMatcher(nameWithLevel) { - return group("name") - } - neuRepoPetItemName.matchMatcher(nameWithLevel) { - return group("name") - } - - return null - } - - fun getPetLevel(nameWithLevel: String): Int? = petItemName.matchMatcher(nameWithLevel) { - group("level").toInt() - } - - fun hasPetName(name: String): Boolean = petItemName.matches(name) && !ignoredPetStrings.any { name.contains(it) } -} diff --git a/src/main/java/at/hannibal2/skyhanni/data/PetData.kt b/src/main/java/at/hannibal2/skyhanni/data/PetData.kt new file mode 100644 index 000000000000..aeef00974fff --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/data/PetData.kt @@ -0,0 +1,216 @@ +package at.hannibal2.skyhanni.data + +import at.hannibal2.skyhanni.api.CurrentPetApi.petDespawnMenuPattern +import at.hannibal2.skyhanni.data.jsonobjects.repo.neu.NeuPetSkinJson +import at.hannibal2.skyhanni.test.command.ErrorManager +import at.hannibal2.skyhanni.utils.ItemUtils.getLore +import at.hannibal2.skyhanni.utils.LorenzColor +import at.hannibal2.skyhanni.utils.LorenzRarity +import at.hannibal2.skyhanni.utils.NeuInternalName +import at.hannibal2.skyhanni.utils.NeuInternalName.Companion.toInternalName +import at.hannibal2.skyhanni.utils.NeuItems.getItemStackOrNull +import at.hannibal2.skyhanni.utils.PetUtils +import at.hannibal2.skyhanni.utils.PetUtils.getSkinOrNull +import at.hannibal2.skyhanni.utils.PetUtils.levelToXp +import at.hannibal2.skyhanni.utils.PetUtils.xpToLevel +import at.hannibal2.skyhanni.utils.RegexUtils.anyMatches +import at.hannibal2.skyhanni.utils.SkyBlockItemModifierUtils.getExtraAttributes +import at.hannibal2.skyhanni.utils.StringUtils.firstLetterUppercase +import at.hannibal2.skyhanni.utils.StringUtils.removeColor +import com.google.gson.Gson +import com.google.gson.annotations.Expose +import net.minecraft.item.ItemStack + +data class PetDataStorage( + @Expose var petItem: NeuInternalName? = null, // The internal name of the pet, e.g., `RABBIT;5` + @Expose var heldItem: NeuInternalName? = null, // The held item of the pet, e.g., `PET_ITEM_COMBAT_SKILL_BOOST_EPIC` + @Expose var cleanName: String? = null, // The clean name of the pet, e.g., `Rabbit` + @Expose var skinSymbolColor: LorenzColor? = null, // The color symbol of the skin of the pet, e.g., §d ✦ -> `LorenzColor.Pink` + @Expose var rarity: LorenzRarity? = null, // The rarity of the pet, e.g., `COMMON` + @Expose var level: Int? = null, // The current level of the pet as an integer, e.g., `100` + @Expose var xp: Double? = null, // The total XP of the pet as a double, e.g., `0.0` + @Expose var skinInternalNameOverride: NeuInternalName? = null, // If the skin is known (i.e., from stored data or Inventory) +) { + fun toPetData(): PetData = PetData( + petItem = petItem, + heldItem = heldItem, + cleanName = cleanName, + skinSymbolColor = skinSymbolColor, + rarity = rarity, + level = level, + xp = xp, + skinInternalNameOverride = skinInternalNameOverride, + ) +} + +data class PetData( + val petItem: NeuInternalName? = null, + val heldItem: NeuInternalName? = null, + val cleanName: String? = null, + val skinSymbolColor: LorenzColor? = null, + val rarity: LorenzRarity? = null, + val level: Int? = null, + val xp: Double? = null, + val skinInternalNameOverride: NeuInternalName? = null, +) { + val displayName = "${rarity?.chatColorCode}$cleanName" + val skin: NeuPetSkinJson? = getSkinOrNull() + + val levelProgressionPercentage: Double? = when { + xp == null -> null + level == null -> null + petItem == null -> null + PetUtils.isValidLevel(level + 1, petItem) -> { + val currentLevelXp = levelToXp(level, petItem) ?: 0.0 + val nextLevelXp = levelToXp(level + 1, petItem) ?: 0.0 + val xpDifference = nextLevelXp - currentLevelXp + val xpProgress = xp - currentLevelXp + xpProgress / xpDifference * 100 + } + else -> 100.0 + } + + @Expose var skinInternalName: NeuInternalName? = skinInternalNameOverride ?: skin?.internalName + + // Please god only use this for UI, not for comparisons + fun getUserFriendlyName( + includeLevel: Boolean = true, + includeSkin: Boolean = true, + ): String { + val levelString = if (includeLevel) "§7[Lvl $level] §r" else "" + val skinString = if (includeSkin) skinSymbolColor?.let { "${it.getChatColor()}✦" }.orEmpty() else "" + return "§r$levelString$displayName$skinString" + } + + fun getItemStackOrNull(): ItemStack? = skin?.itemStack ?: petItem?.getItemStackOrNull() + + override fun equals(other: Any?): Boolean { + if (other !is PetData) return false + return allButSkinEquivalent(other) && this.skinInternalName == other.skinInternalName + } + + fun allButSkinEquivalent(other: Any?): Boolean { + if (other !is PetData) return false + return this.petItem == other.petItem && + this.heldItem == other.heldItem && + this.cleanName == other.cleanName && + this.rarity == other.rarity && + this.level == other.level && + this.xp == other.xp + } + + override fun hashCode(): Int { + var result = cleanName.hashCode() + result = 31 * result + rarity.hashCode() + result = 31 * result + (heldItem?.hashCode() ?: 0) + result = 31 * result + (level ?: 0) + return result + } + + fun isInitialized(): Boolean { + return petItem != null && cleanName != null && rarity != null && level != null && xp != null + } + + fun asStorage(): PetDataStorage = PetDataStorage( + petItem = petItem, + heldItem = heldItem, + cleanName = cleanName, + skinSymbolColor = skinSymbolColor, + rarity = rarity, + level = level, + xp = xp, + skinInternalNameOverride = skinInternalNameOverride, + ) + + companion object { + // + fun parsePetData( + lines: List, + itemHandler: (String) -> NeuInternalName?, + xpHandler: (String) -> Double?, + petHandler: (String) -> PetData? + ): Pair? { + return parsePetDataLists( + lines, + itemHandlerList = { it.firstNotNullOfOrNull(itemHandler) }, + xpHandlerList = { it.firstNotNullOfOrNull(xpHandler) }, + petHandlerList = { it.firstNotNullOfOrNull(petHandler) } + ) + } + + fun parsePetDataLists( + lines: List, + itemHandlerList: (List) -> NeuInternalName?, + xpHandlerList: (List) -> Double?, + petHandlerList: (List) -> PetData? + ): Pair? { + val petItem = itemHandlerList(lines) ?: return null + val overflowXP = xpHandlerList(lines) ?: 0.0 + + val data = petHandlerList(lines) ?: return null + val petData = PetData( + petItem = data.petItem, + cleanName = data.cleanName, + rarity = data.rarity, + heldItem = petItem, + level = data.level, + xp = data.xp, + ) + + return petData to overflowXP + } + + private fun parseFromItem(item: ItemStack): PetData { + val petInfo = Gson().fromJson(item.getExtraAttributes()?.getString("petInfo"), PetNBT::class.java) + + val petName = petInfo.type + val petRarity = LorenzRarity.getByName(petInfo.tier) ?: ErrorManager.skyHanniError( + "Couldn't parse pet rarity.", + Pair("petNBT", petInfo), + Pair("rarity", petInfo.tier), + ) + val internalName = petNameToInternalName(petName, petRarity) + val level = xpToLevel(petInfo.exp, internalName) ?: 0 + + return PetData( + petItem = internalName, + cleanName = petName.firstLetterUppercase(), + level = level, + rarity = petRarity, + heldItem = petInfo.heldItem?.toInternalName(), + xp = petInfo.exp, + ) + } + + fun petNameToInternalName(name: String, rarity: LorenzRarity): NeuInternalName = + "${name.removeColor()};${rarity.id}".toInternalName() + + fun internalNameToPetName(internalName: NeuInternalName): Pair? { + val (name, rarityStr) = internalName.asString().split(";") + val rarity = LorenzRarity.getById(rarityStr.toInt()) ?: return null + return Pair(name, rarity) + } + + fun parsePetAsItem(item: ItemStack): PetData? { + val lore = item.getLore() + if (petDespawnMenuPattern.anyMatches(lore)) return null + return parseFromItem(item) + } + // + } +} + +data class PetNBT( + val type: String, + val active: Boolean, + val exp: Double, + val tier: String, + val hideInfo: Boolean, + val heldItem: String?, + val candyUsed: Int, + val skin: String?, + val uuid: String, + val uniqueId: String, + val hideRightClick: Boolean, + val noMove: Boolean, +) diff --git a/src/main/java/at/hannibal2/skyhanni/data/jsonobjects/repo/NEUPetsJson.kt b/src/main/java/at/hannibal2/skyhanni/data/jsonobjects/repo/NEUPetsJson.kt new file mode 100644 index 000000000000..a15a2a664a27 --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/data/jsonobjects/repo/NEUPetsJson.kt @@ -0,0 +1,21 @@ +package at.hannibal2.skyhanni.data.jsonobjects.repo + +import at.hannibal2.skyhanni.utils.LorenzRarity +import at.hannibal2.skyhanni.utils.NeuInternalName +import com.google.gson.annotations.Expose +import com.google.gson.annotations.SerializedName + +data class NEUPetsJson( + @Expose @SerializedName("pet_rarity_offset") val petRarityOffset: Map, + @Expose @SerializedName("pet_levels") val petLevels: List, + @Expose @SerializedName("custom_pet_leveling") val customPetLeveling: Map, + @Expose @SerializedName("id_to_display_name") val internalToDisplayName: Map +) + +data class NEUPetData( + @Expose @SerializedName("type") val type: Int? = null, + @Expose @SerializedName("pet_levels") val petLevels: List? = null, + @Expose @SerializedName("max_level") val maxLevel: Int? = null, + @Expose @SerializedName("rarity_offset") val rarityOffset: Map? = null, + @Expose @SerializedName("xp_multiplier") val xpMultiplier: Double? = null, +) diff --git a/src/main/java/at/hannibal2/skyhanni/data/jsonobjects/repo/neu/NeuPetSkinJson.kt b/src/main/java/at/hannibal2/skyhanni/data/jsonobjects/repo/neu/NeuPetSkinJson.kt new file mode 100644 index 000000000000..cd83241875d9 --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/data/jsonobjects/repo/neu/NeuPetSkinJson.kt @@ -0,0 +1,68 @@ +package at.hannibal2.skyhanni.data.jsonobjects.repo.neu + +import at.hannibal2.skyhanni.api.CurrentPetApi +import at.hannibal2.skyhanni.skyhannimodule.SkyHanniModule +import at.hannibal2.skyhanni.utils.ItemUtils +import at.hannibal2.skyhanni.utils.ItemUtils.getSkullTexture +import at.hannibal2.skyhanni.utils.LorenzRarity +import at.hannibal2.skyhanni.utils.NeuInternalName.Companion.toInternalName +import at.hannibal2.skyhanni.utils.RegexUtils.firstMatcher +import com.google.gson.annotations.Expose +import com.google.gson.annotations.SerializedName +import net.minecraft.nbt.CompressedStreamTools +import net.minecraft.nbt.NBTTagCompound +import java.io.ByteArrayInputStream +import java.util.Base64 + +data class NeuPetSkinJson( + @Expose @SerializedName("itemid") val itemId: String, + @Expose @SerializedName("displayname") val displayName: String, + @Expose @SerializedName("nbttag") val nbtTagString: String, + @Expose val damage: Int, + @Expose val lore: List, + @Expose @SerializedName("internalname") val internalNameStr: String, + @Expose @SerializedName("crafttext") val craftText: String, + @Expose @SerializedName("clickcommand") val clickCommand: String, + @Expose @SerializedName("modver") val modVersion: String, + @Expose val infoType: String, + @Expose val info: List +) { + /** + * Parses the NBT tag from the JSON into an NBTTagCompound. + * @return Parsed NBTTagCompound object. + * @throws IllegalArgumentException if the NBT parsing fails. + */ + private val nbtTag: NBTTagCompound? get() = try { + val decodedBytes = Base64.getDecoder().decode(nbtTagString.toByteArray(Charsets.UTF_8)) + val inputStream = ByteArrayInputStream(decodedBytes) + CompressedStreamTools.readCompressed(inputStream) + } catch (e: Exception) { + throw IllegalArgumentException("Failed to parse NBT tag: $nbtTagString", e) + } + + @Suppress("SpreadOperator") + val itemStack by lazy { + nbtTag?.let { + ItemUtils.createSkull( + displayName, + it.getString("ID"), + it.getSkullTexture(), + *lore.toTypedArray() + ) + } + } + val rarity: LorenzRarity? = rarityPattern.firstMatcher(lore) { LorenzRarity.getByName(group("rarity")) } + val internalName = internalNameStr.toInternalName() + + @SkyHanniModule + companion object { + /** + * REGEX-TEST: §9§lRARE COSMETIC + * REGEX-TEST: §d§lMYTHIC COSMETIC + */ + private val rarityPattern by CurrentPetApi.patternGroup.pattern( + "skin.rarity", + "(?:§.)+(?[A-Z]+) COSMETIC", + ) + } +} diff --git a/src/main/java/at/hannibal2/skyhanni/events/skyblock/PetChangeEvent.kt b/src/main/java/at/hannibal2/skyhanni/events/skyblock/PetChangeEvent.kt new file mode 100644 index 000000000000..c59a3e7a9b3f --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/events/skyblock/PetChangeEvent.kt @@ -0,0 +1,13 @@ +package at.hannibal2.skyhanni.events.skyblock + +import at.hannibal2.skyhanni.api.event.SkyHanniEvent +import at.hannibal2.skyhanni.data.PetData + +/** + * This event fires when a pet change occurs and when joining SkyBlock for the first time in a session. + * The XP value in the PetData might not be accurate. + * + * @property oldPet The previous pet before the change. + * @property newPet The new pet after the change. + */ +class PetChangeEvent(val oldPet: PetData?, val newPet: PetData?) : SkyHanniEvent() diff --git a/src/main/java/at/hannibal2/skyhanni/features/event/diana/DianaApi.kt b/src/main/java/at/hannibal2/skyhanni/features/event/diana/DianaApi.kt index 524653d83fba..9087c0c44578 100644 --- a/src/main/java/at/hannibal2/skyhanni/features/event/diana/DianaApi.kt +++ b/src/main/java/at/hannibal2/skyhanni/features/event/diana/DianaApi.kt @@ -1,9 +1,9 @@ package at.hannibal2.skyhanni.features.event.diana +import at.hannibal2.skyhanni.api.CurrentPetApi import at.hannibal2.skyhanni.api.event.HandleEvent import at.hannibal2.skyhanni.data.IslandType import at.hannibal2.skyhanni.data.Perk -import at.hannibal2.skyhanni.data.PetApi import at.hannibal2.skyhanni.events.diana.InquisitorFoundEvent import at.hannibal2.skyhanni.events.entity.EntityEnterWorldEvent import at.hannibal2.skyhanni.skyhannimodule.SkyHanniModule @@ -24,7 +24,7 @@ object DianaApi { private fun isRitualActive() = Perk.MYTHOLOGICAL_RITUAL.isActive || Perk.PERKPOCALYPSE.isActive - fun hasGriffinPet() = PetApi.isCurrentPet("Griffin") + fun hasGriffinPet() = CurrentPetApi.isCurrentPet("Griffin") fun isDoingDiana() = IslandType.HUB.isInIsland() && isRitualActive() && hasSpadeInInventory() diff --git a/src/main/java/at/hannibal2/skyhanni/features/event/hoppity/MythicRabbitPetWarning.kt b/src/main/java/at/hannibal2/skyhanni/features/event/hoppity/MythicRabbitPetWarning.kt index c6cc17f0c64b..c60d9ec218a9 100644 --- a/src/main/java/at/hannibal2/skyhanni/features/event/hoppity/MythicRabbitPetWarning.kt +++ b/src/main/java/at/hannibal2/skyhanni/features/event/hoppity/MythicRabbitPetWarning.kt @@ -1,13 +1,14 @@ package at.hannibal2.skyhanni.features.event.hoppity -import at.hannibal2.skyhanni.data.PetApi +import at.hannibal2.skyhanni.api.CurrentPetApi import at.hannibal2.skyhanni.utils.ChatUtils import at.hannibal2.skyhanni.utils.LorenzUtils +import at.hannibal2.skyhanni.utils.NeuInternalName.Companion.toInternalName import at.hannibal2.skyhanni.utils.SimpleTimeMark import kotlin.time.Duration.Companion.seconds object MythicRabbitPetWarning { - private const val MYTHIC_RABBIT_DISPLAY_NAME = "§dRabbit" + private val MYTHIC_RABBIT = "RABBIT;5".toInternalName() private var lastCheck = SimpleTimeMark.farPast() fun check() { @@ -21,7 +22,7 @@ object MythicRabbitPetWarning { } } - fun correctPet() = PetApi.isCurrentPet(MYTHIC_RABBIT_DISPLAY_NAME) + fun correctPet() = CurrentPetApi.isCurrentPet(MYTHIC_RABBIT) private fun warn() { ChatUtils.chat("Use a §dMythic Rabbit Pet §efor more chocolate!") diff --git a/src/main/java/at/hannibal2/skyhanni/features/garden/GardenApi.kt b/src/main/java/at/hannibal2/skyhanni/features/garden/GardenApi.kt index 9771265c155a..b74873387292 100644 --- a/src/main/java/at/hannibal2/skyhanni/features/garden/GardenApi.kt +++ b/src/main/java/at/hannibal2/skyhanni/features/garden/GardenApi.kt @@ -1,9 +1,9 @@ package at.hannibal2.skyhanni.features.garden import at.hannibal2.skyhanni.SkyHanniMod +import at.hannibal2.skyhanni.api.CurrentPetApi import at.hannibal2.skyhanni.api.event.HandleEvent import at.hannibal2.skyhanni.data.IslandType -import at.hannibal2.skyhanni.data.PetApi import at.hannibal2.skyhanni.data.ProfileStorageData import at.hannibal2.skyhanni.data.jsonobjects.repo.GardenJson import at.hannibal2.skyhanni.events.BlockClickEvent @@ -58,7 +58,7 @@ object GardenApi { var itemInHand: ItemStack? = null var cropInHand: CropType? = null val mushroomCowPet - get() = PetApi.isCurrentPet("Mooshroom Cow") && + get() = CurrentPetApi.isCurrentPet("Mooshroom Cow") && storage?.fortune?.farmingItems?.get(FarmingItems.MOOSHROOM_COW) ?.let { it.getItemRarityOrNull()?.isAtLeast(LorenzRarity.RARE) } ?: false private var inBarn = false diff --git a/src/main/java/at/hannibal2/skyhanni/features/garden/fortuneguide/CaptureFarmingGear.kt b/src/main/java/at/hannibal2/skyhanni/features/garden/fortuneguide/CaptureFarmingGear.kt index 9e648f25ae0e..23e65d3daa53 100644 --- a/src/main/java/at/hannibal2/skyhanni/features/garden/fortuneguide/CaptureFarmingGear.kt +++ b/src/main/java/at/hannibal2/skyhanni/features/garden/fortuneguide/CaptureFarmingGear.kt @@ -3,7 +3,6 @@ package at.hannibal2.skyhanni.features.garden.fortuneguide import at.hannibal2.skyhanni.api.event.HandleEvent import at.hannibal2.skyhanni.config.ConfigUpdaterMigrator import at.hannibal2.skyhanni.config.storage.ProfileSpecificStorage -import at.hannibal2.skyhanni.data.PetApi import at.hannibal2.skyhanni.data.ProfileStorageData import at.hannibal2.skyhanni.events.GardenToolChangeEvent import at.hannibal2.skyhanni.events.InventoryFullyOpenedEvent @@ -22,6 +21,7 @@ import at.hannibal2.skyhanni.utils.ItemUtils.getItemRarityOrNull import at.hannibal2.skyhanni.utils.ItemUtils.getLore import at.hannibal2.skyhanni.utils.NumberUtil.romanToDecimal import at.hannibal2.skyhanni.utils.NumberUtil.romanToDecimalIfNecessary +import at.hannibal2.skyhanni.utils.PetUtils.isPetMenu import at.hannibal2.skyhanni.utils.RegexUtils.firstMatcher import at.hannibal2.skyhanni.utils.RegexUtils.matchMatcher import at.hannibal2.skyhanni.utils.SimpleTimeMark.Companion.fromNow @@ -183,7 +183,7 @@ object CaptureFarmingGear { val storage = GardenApi.storage?.fortune ?: return val outdatedItems = outdatedItems ?: return val items = event.inventoryItems - if (PetApi.isPetMenu(event.inventoryName)) { + if (isPetMenu(event.inventoryName, event.inventoryItems)) { pets(items, outdatedItems) return } diff --git a/src/main/java/at/hannibal2/skyhanni/features/garden/fortuneguide/FFStats.kt b/src/main/java/at/hannibal2/skyhanni/features/garden/fortuneguide/FFStats.kt index c7afa9dc0644..ac1d95eaa3e3 100644 --- a/src/main/java/at/hannibal2/skyhanni/features/garden/fortuneguide/FFStats.kt +++ b/src/main/java/at/hannibal2/skyhanni/features/garden/fortuneguide/FFStats.kt @@ -162,10 +162,10 @@ object FFStats { FFGuideGUI.updateDisplay() } - fun List.getFFData(): Map = combineFFData(this.map { it.getFFData() }) + private fun List.getFFData(): Map = combineFFData(this.map { it.getFFData() }) - fun combineFFData(vararg value: Map) = combineFFData(value.toList()) - fun combineFFData(value: List>) = + private fun combineFFData(vararg value: Map) = combineFFData(value.toList()) + private fun combineFFData(value: List>) = value.map { it.toList() }.flatten().groupBy({ it.first }, { it.second }) .mapValues { (_, values) -> values.sum() } diff --git a/src/main/java/at/hannibal2/skyhanni/features/inventory/ItemDisplayOverlayFeatures.kt b/src/main/java/at/hannibal2/skyhanni/features/inventory/ItemDisplayOverlayFeatures.kt index b2c7f0f54c02..4a00a85aebee 100644 --- a/src/main/java/at/hannibal2/skyhanni/features/inventory/ItemDisplayOverlayFeatures.kt +++ b/src/main/java/at/hannibal2/skyhanni/features/inventory/ItemDisplayOverlayFeatures.kt @@ -2,6 +2,7 @@ package at.hannibal2.skyhanni.features.inventory import at.hannibal2.skyhanni.SkyHanniMod import at.hannibal2.skyhanni.api.CollectionApi +import at.hannibal2.skyhanni.api.CurrentPetApi import at.hannibal2.skyhanni.api.SkillApi import at.hannibal2.skyhanni.api.event.HandleEvent import at.hannibal2.skyhanni.config.ConfigUpdaterMigrator @@ -25,7 +26,6 @@ import at.hannibal2.skyhanni.config.features.inventory.InventoryConfig.ItemNumbe import at.hannibal2.skyhanni.config.features.inventory.InventoryConfig.ItemNumberEntry.SKYBLOCK_LEVEL import at.hannibal2.skyhanni.config.features.inventory.InventoryConfig.ItemNumberEntry.TIME_POCKET_ITEMS import at.hannibal2.skyhanni.config.features.inventory.InventoryConfig.ItemNumberEntry.VACUUM_GARDEN -import at.hannibal2.skyhanni.data.PetApi import at.hannibal2.skyhanni.events.RenderItemTipEvent import at.hannibal2.skyhanni.features.garden.GardenApi import at.hannibal2.skyhanni.features.garden.pests.PestApi @@ -74,6 +74,7 @@ object ItemDisplayOverlayFeatures { "masterskull.id", "MASTER_SKULL_TIER_(?\\d)", ) + /** * REGEX-TEST: §7Vacuum Bag: §21 Pest * REGEX-TEST: §7Vacuum Bag: §2444 Pests @@ -231,7 +232,7 @@ object ItemDisplayOverlayFeatures { if (RANCHERS_BOOTS_SPEED.isSelected() && internalName == "RANCHERS_BOOTS".toInternalName()) { item.getRanchersSpeed()?.let { - val isUsingBlackCat = PetApi.isCurrentPet("Black Cat") + val isUsingBlackCat = CurrentPetApi.isCurrentPet("Black Cat") val helmet = InventoryUtils.getHelmet()?.getInternalName() val hand = InventoryUtils.getItemInHand()?.getInternalName() val racingHelmet = "RACING_HELMET".toInternalName() diff --git a/src/main/java/at/hannibal2/skyhanni/features/inventory/experimentationtable/ExperimentationTableApi.kt b/src/main/java/at/hannibal2/skyhanni/features/inventory/experimentationtable/ExperimentationTableApi.kt index 6c11bcfde431..1e545bbbd2ab 100644 --- a/src/main/java/at/hannibal2/skyhanni/features/inventory/experimentationtable/ExperimentationTableApi.kt +++ b/src/main/java/at/hannibal2/skyhanni/features/inventory/experimentationtable/ExperimentationTableApi.kt @@ -1,8 +1,8 @@ package at.hannibal2.skyhanni.features.inventory.experimentationtable +import at.hannibal2.skyhanni.api.CurrentPetApi import at.hannibal2.skyhanni.api.event.HandleEvent import at.hannibal2.skyhanni.data.IslandType -import at.hannibal2.skyhanni.data.PetApi import at.hannibal2.skyhanni.data.ProfileStorageData import at.hannibal2.skyhanni.events.InventoryCloseEvent import at.hannibal2.skyhanni.events.InventoryUpdatedEvent @@ -23,11 +23,10 @@ import net.minecraft.entity.item.EntityArmorStand object ExperimentationTableApi { private val storage get() = ProfileStorageData.profileSpecific?.experimentation + private val inTable get() = inventoriesPattern.matches(openInventoryName()) + private val EXPERIMENTATION_TABLE_SKULL by lazy { SkullTextureHolder.getTexture("EXPERIMENTATION_TABLE") } private val patternGroup = RepoPattern.group("enchanting.experiments") - private val EXPERIMENTATION_TABLE_SKULL by lazy { SkullTextureHolder.getTexture("EXPERIMENTATION_TABLE") } - private val inTable get() = inventoriesPattern.matches(openInventoryName()) - var currentExperiment: Experiment? = null val superpairInventory = InventoryDetector( openInventory = { name -> currentExperiment = superpairsPattern.matchMatcher(name) { @@ -36,6 +35,9 @@ object ExperimentationTableApi { }, ) { name -> inventoriesPattern.matches(name) } + var currentExperiment: Experiment? = null + private set + // /** * REGEX-TEST: Superpairs (Metaphysical) @@ -152,15 +154,6 @@ object ExperimentationTableApi { "book", "§9(?.*)", ) - - /** - * REGEX-TEST: §dGuardian - * REGEX-TEST: §9Guardian§e - */ - private val petNamePattern by patternGroup.pattern( - "guardianpet", - "§[956d]Guardian.*", - ) // fun inDistanceToTable(max: Double): Boolean { @@ -185,5 +178,5 @@ object ExperimentationTableApi { }?.getLorenzVec().takeIf { it != storage?.tablePos } ?: return } - fun hasGuardianPet(): Boolean = petNamePattern.matches(PetApi.currentPet) + fun guardianPetActive(): Boolean = CurrentPetApi.currentPet?.cleanName == "Guardian" } diff --git a/src/main/java/at/hannibal2/skyhanni/features/inventory/experimentationtable/GuardianReminder.kt b/src/main/java/at/hannibal2/skyhanni/features/inventory/experimentationtable/GuardianReminder.kt index d9660374a9e7..0e2b69909880 100644 --- a/src/main/java/at/hannibal2/skyhanni/features/inventory/experimentationtable/GuardianReminder.kt +++ b/src/main/java/at/hannibal2/skyhanni/features/inventory/experimentationtable/GuardianReminder.kt @@ -40,7 +40,7 @@ object GuardianReminder { } private fun warn() { - if (ExperimentationTableApi.hasGuardianPet()) return + if (ExperimentationTableApi.guardianPetActive()) return ChatUtils.clickToActionOrDisable( "Use a §9§lGuardian Pet §efor more Exp in the Experimentation Table.", @@ -55,7 +55,7 @@ object GuardianReminder { if (!isEnabled()) return if (InventoryUtils.openInventoryName() != "Experimentation Table") return if (lastInventoryOpen.passedSince() > 2.seconds) return - if (ExperimentationTableApi.hasGuardianPet()) return + if (ExperimentationTableApi.guardianPetActive()) return val gui = Minecraft.getMinecraft().currentScreen as? GuiContainer ?: return sendTitle(gui.width, gui.height) diff --git a/src/main/java/at/hannibal2/skyhanni/features/misc/discordrpc/DiscordStatus.kt b/src/main/java/at/hannibal2/skyhanni/features/misc/discordrpc/DiscordStatus.kt index 2b261557af85..197443c6d0d1 100644 --- a/src/main/java/at/hannibal2/skyhanni/features/misc/discordrpc/DiscordStatus.kt +++ b/src/main/java/at/hannibal2/skyhanni/features/misc/discordrpc/DiscordStatus.kt @@ -2,6 +2,7 @@ package at.hannibal2.skyhanni.features.misc.discordrpc // SkyblockAddons code, adapted for SkyHanni with some additions and fixes +import at.hannibal2.skyhanni.api.CurrentPetApi import at.hannibal2.skyhanni.data.ActionBarStatsData import at.hannibal2.skyhanni.data.GardenCropMilestones.getCounter import at.hannibal2.skyhanni.data.GardenCropMilestones.getTierForCropCount @@ -9,7 +10,6 @@ import at.hannibal2.skyhanni.data.GardenCropMilestones.isMaxed import at.hannibal2.skyhanni.data.GardenCropMilestones.progressToNextLevel import at.hannibal2.skyhanni.data.HypixelData import at.hannibal2.skyhanni.data.IslandType -import at.hannibal2.skyhanni.data.PetApi import at.hannibal2.skyhanni.data.ScoreboardData import at.hannibal2.skyhanni.features.dungeon.DungeonApi import at.hannibal2.skyhanni.features.garden.GardenApi @@ -19,7 +19,6 @@ import at.hannibal2.skyhanni.features.rift.RiftApi import at.hannibal2.skyhanni.utils.InventoryUtils import at.hannibal2.skyhanni.utils.ItemUtils.extraAttributes import at.hannibal2.skyhanni.utils.LorenzUtils -import at.hannibal2.skyhanni.utils.LorenzUtils.colorCodeToRarity import at.hannibal2.skyhanni.utils.NumberUtil.addSeparators import at.hannibal2.skyhanni.utils.SimpleTimeMark import at.hannibal2.skyhanni.utils.SkyBlockTime @@ -28,8 +27,6 @@ import at.hannibal2.skyhanni.utils.StringUtils.removeColor import at.hannibal2.skyhanni.utils.TabListData import at.hannibal2.skyhanni.utils.TimeUtils.format import at.hannibal2.skyhanni.utils.TimeUtils.formatted -import at.hannibal2.skyhanni.utils.system.PlatformUtils -import io.github.moulberry.notenoughupdates.miscfeatures.PetInfoOverlay.getCurrentPet import java.util.regex.Pattern import kotlin.time.Duration.Companion.minutes @@ -73,13 +70,8 @@ private fun getCropMilestoneDisplay(): String { return "${crop.cropName}: $text" } -fun getPetDisplay(): String = PetApi.currentPet?.let { - val colorCode = it.substring(1..2).first() - val petName = it.substring(2).removeColor() - val petLevel = if (PlatformUtils.isNeuLoaded()) getCurrentPet()?.petLevel?.currentLevel ?: "?" else "?" - - "[Lvl $petLevel] ${colorCodeToRarity(colorCode)} $petName" -} ?: "No pet equipped" +private fun getPetDisplay(): String = CurrentPetApi.currentPet?.getUserFriendlyName() + ?: "No pet equipped" enum class DiscordStatus(private val displayMessageSupplier: (() -> String?)) { @@ -108,7 +100,6 @@ enum class DiscordStatus(private val displayMessageSupplier: (() -> String?)) { else -> location.takeIf { it != "None" && it != "invalid" } ?: lastKnownDisplayStrings[LOCATION].orEmpty() } - // Only display None if we don't have a last known area lastKnownDisplayStrings[LOCATION].takeIf { it?.isNotEmpty() == true } ?: "None" }, @@ -275,6 +266,7 @@ enum class DiscordStatus(private val displayMessageSupplier: (() -> String?)) { // Logic for getting the currently held stacking enchant is from Skytils val itemInHand = InventoryUtils.getItemInHand() val itemName = itemInHand?.displayName?.removeColor().orEmpty() + val extraAttributes = itemInHand?.extraAttributes fun getProgressPercent(amount: Int, levels: List): String { var percent = "MAXED" @@ -292,7 +284,6 @@ enum class DiscordStatus(private val displayMessageSupplier: (() -> String?)) { return percent } - val extraAttributes = itemInHand?.extraAttributes var stackingReturn = AutoStatus.STACKING.placeholderText if (extraAttributes != null) { val enchantments = extraAttributes.getCompoundTag("enchantments") diff --git a/src/main/java/at/hannibal2/skyhanni/features/misc/pets/CurrentPetDisplay.kt b/src/main/java/at/hannibal2/skyhanni/features/misc/pets/CurrentPetDisplay.kt index 6eb748d8af71..5e533a4bb33f 100644 --- a/src/main/java/at/hannibal2/skyhanni/features/misc/pets/CurrentPetDisplay.kt +++ b/src/main/java/at/hannibal2/skyhanni/features/misc/pets/CurrentPetDisplay.kt @@ -1,108 +1,60 @@ package at.hannibal2.skyhanni.features.misc.pets import at.hannibal2.skyhanni.SkyHanniMod +import at.hannibal2.skyhanni.api.CurrentPetApi import at.hannibal2.skyhanni.api.event.HandleEvent import at.hannibal2.skyhanni.config.ConfigUpdaterMigrator -import at.hannibal2.skyhanni.data.PetApi import at.hannibal2.skyhanni.events.GuiRenderEvent -import at.hannibal2.skyhanni.events.InventoryFullyOpenedEvent -import at.hannibal2.skyhanni.events.chat.SkyHanniChatEvent import at.hannibal2.skyhanni.features.rift.RiftApi import at.hannibal2.skyhanni.skyhannimodule.SkyHanniModule -import at.hannibal2.skyhanni.utils.ItemUtils.getLore -import at.hannibal2.skyhanni.utils.RegexUtils.firstMatcher -import at.hannibal2.skyhanni.utils.RegexUtils.matchMatcher -import at.hannibal2.skyhanni.utils.RegexUtils.matches -import at.hannibal2.skyhanni.utils.RenderUtils.renderString -import at.hannibal2.skyhanni.utils.repopatterns.RepoPattern +import at.hannibal2.skyhanni.utils.RenderUtils +import at.hannibal2.skyhanni.utils.RenderUtils.renderRenderable +import at.hannibal2.skyhanni.utils.renderables.Renderable +import java.awt.Color @SkyHanniModule object CurrentPetDisplay { - private val config get() = SkyHanniMod.feature.misc.pets - - private val patternGroup = RepoPattern.group("misc.currentpet") - - /** - * REGEX-TEST: §7§7Selected pet: §6Enderman - * REGEX-TEST: §7§7Selected pet: §cNone - */ - private val inventorySelectedPetPattern by patternGroup.pattern( - "inventory.selected", - "§7§7Selected pet: (?.*)", - ) - - /** - * REGEX-TEST: §aYou summoned your §r§6Enderman§r§a! - */ - private val chatSpawnPattern by patternGroup.pattern( - "chat.spawn", - "§aYou summoned your §r(?.*)§r§a!", - ) - - /** - * REGEX-TEST: §aYou despawned your §r§6Enderman§r§a! - */ - private val chatDespawnPattern by patternGroup.pattern( - "chat.despawn", - "§aYou despawned your §r.*§r§a!", - ) - - /** - * REGEX-TEST: §cAutopet §eequipped your §7[Lvl 100] §6Griffin§4 ✦§e! §a§lVIEW RULE - * REGEX-TEST: §cAutopet §eequipped your §7[Lvl 100] §6Elephant§e! §a§lVIEW RULE - */ - private val chatPetRulePattern by patternGroup.pattern( - "chat.rule", - "§cAutopet §eequipped your §7\\[Lvl .*] (?.*)§e! §a§lVIEW RULE", - ) - - @HandleEvent - fun onChat(event: SkyHanniChatEvent) { - findPetInChat(event.message)?.let { - PetApi.currentPet = it - if (config.hideAutopet) { - event.blockedReason = "pets" - } - } - } - - private fun findPetInChat(message: String): String? { - chatSpawnPattern.matchMatcher(message) { - return group("pet") - } - if (chatDespawnPattern.matches(message)) { - return "" - } - chatPetRulePattern.matchMatcher(message) { - return group("pet") - } - - return null - } - - @HandleEvent - fun onInventoryFullyOpened(event: InventoryFullyOpenedEvent) { - if (!PetApi.isPetMenu(event.inventoryName)) return - - val lore = event.inventoryItems[4]?.getLore() ?: return - inventorySelectedPetPattern.firstMatcher(lore) { - val newPet = group("pet") - PetApi.currentPet = if (newPet != "§cNone") newPet else "" - } - } - - @HandleEvent(onlyOnSkyblock = true) - fun onRenderOverlay(event: GuiRenderEvent.GuiOverlayRenderEvent) { - if (RiftApi.inRift()) return - if (!config.display) return - - config.displayPos.renderString(PetApi.currentPet, posLabel = "Current Pet") + private val config get() = SkyHanniMod.feature.misc.pets.display + + @HandleEvent(GuiRenderEvent.GuiOverlayRenderEvent::class, onlyOnSkyblock = true) + fun onRenderOverlay() { + if (RiftApi.inRift() || !config.enabled) return + + val currentPet = CurrentPetApi.currentPet ?: return + val displayName = currentPet.getUserFriendlyName(includeLevel = true) + val itemStack = currentPet.getItemStackOrNull() ?: return + val rarityColor = currentPet.rarity?.color ?: return + + val nameRender = Renderable.string(displayName, color = rarityColor.toColor()) + val circle = Renderable.CircularRenderable( + rarityColor.toColor(), + 20, + itemStack = itemStack, + border = Renderable.CircularRenderable( + Color.GRAY, + 26, + border = if (config.levelRing) Renderable.CircularRenderable( + backgroundColor = Color.cyan, + radius = 29, + filledPercentage = currentPet.levelProgressionPercentage ?: 0.0 + ) else null + ) + ) + + val container = Renderable.verticalContainer( + listOf(nameRender, circle), + horizontalAlign = RenderUtils.HorizontalAlignment.CENTER, + ) + + config.position.renderRenderable(container, posLabel = "Current Pet") } @HandleEvent fun onConfigFix(event: ConfigUpdaterMigrator.ConfigFixEvent) { event.move(3, "misc.petDisplay", "misc.pets.display") event.move(9, "misc.petDisplayPos", "misc.pets.displayPos") + event.move(75, "misc.pets.display", "misc.pets.display.enabled") + event.move(75, "misc.pets.displayPos", "misc.pets.display.pos") } } diff --git a/src/main/java/at/hannibal2/skyhanni/shader/CircleShader.kt b/src/main/java/at/hannibal2/skyhanni/shader/CircleShader.kt new file mode 100644 index 000000000000..b2721e82d932 --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/shader/CircleShader.kt @@ -0,0 +1,29 @@ +package at.hannibal2.skyhanni.shader + +import at.hannibal2.skyhanni.utils.shader.Shader +import at.hannibal2.skyhanni.utils.shader.Uniform +import net.minecraft.client.Minecraft + +object CircleShader : Shader("circle", "circle") { + + val INSTANCE get() = this + + var scaleFactor: Float = 0f + var radius: Float = 0f + var smoothness: Float = 0f + var centerPos: FloatArray = floatArrayOf(0f, 0f) + set(value) { + field = floatArrayOf(value[0], Minecraft.getMinecraft().displayHeight - value[1]) + } + var angle1: Float = 0f + var angle2: Float = 0f + + override fun registerUniforms() { + registerUniform(Uniform.UniformType.FLOAT, "scaleFactor") { scaleFactor } + registerUniform(Uniform.UniformType.FLOAT, "radius") { radius } + registerUniform(Uniform.UniformType.FLOAT, "smoothness") { smoothness } + registerUniform(Uniform.UniformType.FLOAT, "angle1") { angle1 } + registerUniform(Uniform.UniformType.FLOAT, "angle2") { angle2 } + registerUniform(Uniform.UniformType.VEC2, "centerPos") { centerPos } + } +} diff --git a/src/main/java/at/hannibal2/skyhanni/utils/ItemUtils.kt b/src/main/java/at/hannibal2/skyhanni/utils/ItemUtils.kt index c021dc57f174..9e677c37518d 100644 --- a/src/main/java/at/hannibal2/skyhanni/utils/ItemUtils.kt +++ b/src/main/java/at/hannibal2/skyhanni/utils/ItemUtils.kt @@ -5,7 +5,6 @@ import at.hannibal2.skyhanni.api.event.HandleEvent import at.hannibal2.skyhanni.config.commands.CommandCategory import at.hannibal2.skyhanni.config.commands.CommandRegistrationEvent import at.hannibal2.skyhanni.data.NotificationManager -import at.hannibal2.skyhanni.data.PetApi import at.hannibal2.skyhanni.data.SkyHanniNotification import at.hannibal2.skyhanni.data.model.SkyblockStat import at.hannibal2.skyhanni.events.ConfigLoadEvent @@ -24,6 +23,8 @@ import at.hannibal2.skyhanni.utils.NeuItems.getItemStackOrNull import at.hannibal2.skyhanni.utils.NumberUtil.addSeparators import at.hannibal2.skyhanni.utils.NumberUtil.formatInt import at.hannibal2.skyhanni.utils.NumberUtil.shortFormat +import at.hannibal2.skyhanni.utils.PetUtils.getCleanName +import at.hannibal2.skyhanni.utils.PetUtils.petItemNamePattern import at.hannibal2.skyhanni.utils.PrimitiveIngredient.Companion.toPrimitiveItemStacks import at.hannibal2.skyhanni.utils.RegexUtils.matchMatcher import at.hannibal2.skyhanni.utils.RegexUtils.matches @@ -334,19 +335,28 @@ object ItemUtils { LorenzRarity.entries.joinToString(separator = "|") { it.name }, ) - private fun ItemStack.readItemCategoryAndRarity(): Pair { - val cleanName = this.cleanName() + private val ignoredPetStrings = listOf( + "Archer", + "Berserk", + "Mage", + "Tank", + "Healer", + "➡", + ) - if (PetApi.hasPetName(cleanName)) { - return getPetRarity(this) to ItemCategory.PET - } + private fun ItemStack.isPet() = petItemNamePattern.matches(cleanName()) && !ignoredPetStrings.any { + cleanName().contains(it) + } + + private fun ItemStack.readItemCategoryAndRarity(): Pair { + if (this.isPet()) return getPetRarity(this) to ItemCategory.PET for (line in this.getLore().reversed()) { val (category, rarity) = UtilsPatterns.rarityLoreLinePattern.matchMatcher(line) { group("itemCategory").replace(" ", "_") to group("rarity").replace(" ", "_") } ?: continue - val itemCategory = getItemCategory(category, name, cleanName) + val itemCategory = getItemCategory(category) val itemRarity = LorenzRarity.getByName(rarity) if (itemCategory == null) { @@ -381,11 +391,11 @@ object ItemUtils { return null to null } - private fun getItemCategory(itemCategory: String, name: String, cleanName: String = name.removeColor()) = + private fun ItemStack.getItemCategory(itemCategory: String) = if (itemCategory.isEmpty()) when { UtilsPatterns.abiPhonePattern.matches(name) -> ItemCategory.ABIPHONE - PetApi.hasPetName(cleanName) -> ItemCategory.PET - UtilsPatterns.baitPattern.matches(cleanName) -> ItemCategory.FISHING_BAIT + isPet() -> ItemCategory.PET + UtilsPatterns.baitPattern.matches(cleanName()) -> ItemCategory.FISHING_BAIT UtilsPatterns.enchantedBookPattern.matches(name) -> ItemCategory.ENCHANTED_BOOK UtilsPatterns.potionPattern.matches(name) -> ItemCategory.POTION UtilsPatterns.sackPattern.matches(name) -> ItemCategory.SACK @@ -592,7 +602,7 @@ object ItemUtils { } // hide pet level - PetApi.getCleanName(name)?.let { + getCleanName(name)?.let { return "$it Pet" } return name diff --git a/src/main/java/at/hannibal2/skyhanni/utils/LorenzRarity.kt b/src/main/java/at/hannibal2/skyhanni/utils/LorenzRarity.kt index 3d80a4f5dd99..3e2569e6b4b5 100644 --- a/src/main/java/at/hannibal2/skyhanni/utils/LorenzRarity.kt +++ b/src/main/java/at/hannibal2/skyhanni/utils/LorenzRarity.kt @@ -1,6 +1,7 @@ package at.hannibal2.skyhanni.utils import at.hannibal2.skyhanni.test.command.ErrorManager +import at.hannibal2.skyhanni.utils.StringUtils.firstLetterUppercase // TODO: replace id with ordinal enum class LorenzRarity(val color: LorenzColor, val id: Int) { @@ -45,6 +46,15 @@ enum class LorenzRarity(val color: LorenzColor, val id: Int) { return rarityBelow } + fun getCleanName(): String { + val wordArray = rawName.split("_").toTypedArray() + val returnList = mutableListOf() + for (word in wordArray) { + returnList.add(word.firstLetterUppercase()) + } + return returnList.joinToString(" ") + } + fun isAtLeast(other: LorenzRarity): Boolean = this.ordinal >= other.ordinal companion object { @@ -52,5 +62,7 @@ enum class LorenzRarity(val color: LorenzColor, val id: Int) { fun getById(id: Int) = if (entries.size > id) entries[id] else null fun getByName(name: String): LorenzRarity? = entries.find { it.name.equals(name, ignoreCase = true) } + + fun getByColorCode(colorCode: Char): LorenzRarity? = entries.find { it.color.chatColorCode == colorCode } } } diff --git a/src/main/java/at/hannibal2/skyhanni/utils/PetUtils.kt b/src/main/java/at/hannibal2/skyhanni/utils/PetUtils.kt new file mode 100644 index 000000000000..c63f682c15e1 --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/utils/PetUtils.kt @@ -0,0 +1,231 @@ +package at.hannibal2.skyhanni.utils + +import at.hannibal2.skyhanni.api.CurrentPetApi +import at.hannibal2.skyhanni.api.event.HandleEvent +import at.hannibal2.skyhanni.config.commands.CommandCategory +import at.hannibal2.skyhanni.config.commands.CommandRegistrationEvent +import at.hannibal2.skyhanni.data.PetData +import at.hannibal2.skyhanni.data.PetData.Companion.internalNameToPetName +import at.hannibal2.skyhanni.data.PetData.Companion.petNameToInternalName +import at.hannibal2.skyhanni.data.jsonobjects.repo.NEUPetData +import at.hannibal2.skyhanni.data.jsonobjects.repo.NEUPetsJson +import at.hannibal2.skyhanni.data.jsonobjects.repo.neu.NeuPetSkinJson +import at.hannibal2.skyhanni.events.NeuRepositoryReloadEvent +import at.hannibal2.skyhanni.skyhannimodule.SkyHanniModule +import at.hannibal2.skyhanni.test.command.ErrorManager +import at.hannibal2.skyhanni.utils.ItemUtils.getLore +import at.hannibal2.skyhanni.utils.NeuInternalName.Companion.toInternalName +import at.hannibal2.skyhanni.utils.NumberUtil.addSeparators +import at.hannibal2.skyhanni.utils.RegexUtils.matchMatcher +import at.hannibal2.skyhanni.utils.RegexUtils.matches +import at.hannibal2.skyhanni.utils.repopatterns.RepoPattern +import com.google.gson.Gson +import net.minecraft.item.ItemStack + +@SkyHanniModule +object PetUtils { + private val patternGroup = RepoPattern.group("misc.pet") + private const val FORGE_BACK_SLOT = 48 + // Map of Pet Name to a Map of InternalName to NeuPetSkinJson + private val petSkins = mutableMapOf>() + + private var baseXpLevelReqs: List = listOf() + private var baseRarityOffsets: Map = mapOf() + private var customXpLevelReqs: Map? = null + private fun getXpList(petInternalName: NeuInternalName): List = + baseXpLevelReqs + customXpLevelReqs?.get(petInternalName)?.petLevels.orEmpty() + + // + /** + * REGEX-TEST: §e⭐ §7[Lvl 200] §6Golden Dragon§d ✦ + * REGEX-TEST: ⭐ [Lvl 100] Black Cat ✦ + */ + val petItemNamePattern by CurrentPetApi.patternGroup.pattern( + "item.name", + "(?(?:§.)*⭐ )?(?:§.)*\\[Lvl (?\\d+)] (?.*)", + ) + + /** + * REGEX-TEST: Pets (1/3) + * REGEX-TEST: Pets + * REGEX-TEST: Pets (1/4) + * REGEX-TEST: Pets (1/2) + */ + private val petMenuPattern by patternGroup.pattern( + "menu.title", + "Pets(?: \\(\\d+/\\d+\\) )?", + ) + + /** + * REGEX-TEST: §7[Lvl 1➡200] §6Golden Dragon + * REGEX-TEST: §7[Lvl {LVL}] §6Golden Dragon + */ + private val neuRepoPetItemNamePattern by CurrentPetApi.patternGroup.pattern( + "item.name.neu.format", + "(?:§f§f)?§7\\[Lvl (?:1➡(?:100|200)|\\{LVL})] (?.*)", + ) + + /** + * REGEX-TEST: §7To Select Process (Slot #2) + * REGEX-TEST: §7To Select Process (Slot #4) + * REGEX-TEST: §7To Select Process (Slot #7) + * REGEX-TEST: §7To Select Process + */ + private val forgeBackMenuPattern by CurrentPetApi.patternGroup.pattern( + "menu.forge.goback", + "§7To Select Process(?: \\(Slot #\\d\\))?", + ) + + /** + * REGEX-TEST: PET_SKIN_ENDERMAN + * REGEX-TEST: PET_SKIN_PARROT_TOUCAN + * REGEX-TEST: PET_SKIN_PHEONIX_FLAMINGO + * REGEX-TEST: PET_SKIN_PHOENIX_ICE + * REGEX-TEST: PET_SKIN_PIGMAN_LUNAR_PIG + * REGEX-TEST: PET_SKIN_RABBIT + * REGEX-TEST: PET_SKIN_RABBIT_AQUAMARINE + * REGEX-TEST: PET_SKIN_RABBIT_LUNAR + * REGEX-TEST: PET_SKIN_RABBIT_LUNAR_BABY + * REGEX-TEST: PET_SKIN_RABBIT_PLUSHIE + * REGEX-TEST: PET_SKIN_RABBIT_ROSE + */ + private val petSkinNamePattern by CurrentPetApi.patternGroup.pattern( + "neu.pet", + "PET_SKIN_(?[A-Z])_?(?[A-Z_]+)?" + ) + // + + // + fun isPetMenu(inventoryTitle: String, inventoryItems: Map): Boolean { + if (!petMenuPattern.matches(inventoryTitle)) return false + + // Otherwise make sure they're not in the Forge menu looking at pets + return inventoryItems[FORGE_BACK_SLOT]?.getLore().orEmpty().none { + forgeBackMenuPattern.matches(it) + } + } + + fun getCleanName(nameWithLevel: String): String? { + petItemNamePattern.matchMatcher(nameWithLevel) { + return group("name") + } + neuRepoPetItemNamePattern.matchMatcher(nameWithLevel) { + return group("name") + } + + return null + } + + fun rarityByColorGroup(color: String): LorenzRarity = LorenzRarity.getByColorCode(color[0]) + ?: ErrorManager.skyHanniError( + "Unknown rarity", + Pair("rarity", color), + ) + + private fun levelToXPCommand(input: Array) { + if (input.size < 3) { + ChatUtils.userError("Usage: /shcalcpetxp ") + return + } + + val level = input[0].toIntOrNull() + if (level == null) { + ChatUtils.userError("Invalid level '${input[0]}'.") + return + } + val rarity = LorenzRarity.getByName(input[1]) + if (rarity == null) { + ChatUtils.userError("Invalid rarity '${input[1]}'.") + return + } + + val petName = input.slice(2.. 0 } ?: return null + var level = 0 + for (i in 0 + rarityOffset until xpList.size) { + val xpReq = xpList[i] + if (xp >= xpReq) { + xp -= xpReq + level++ + } else break + } + + return level + } + + fun isValidLevel(level: Int, petInternalName: NeuInternalName): Boolean = + level <= (customXpLevelReqs?.get(petInternalName)?.maxLevel ?: 100) + + private fun getRarityOffset(petInternalName: NeuInternalName): Int { + val rarityOffset = customXpLevelReqs?.get(petInternalName)?.rarityOffset ?: baseRarityOffsets + val (_, rarity) = internalNameToPetName(petInternalName) ?: return 0 + return rarityOffset[rarity] ?: 0 + } + // + + @HandleEvent + fun onNeuRepoReload(event: NeuRepositoryReloadEvent) { + val data = event.getConstant("pets") + baseXpLevelReqs = data.petLevels + customXpLevelReqs = data.customPetLeveling.mapKeys { it.key.toInternalName() } + baseRarityOffsets = data.petRarityOffset + + NeuItems.allNeuRepoItems().forEach { (rawInternalName, jsonObject) -> + petSkinNamePattern.matchMatcher(rawInternalName) { + val petName = group("pet") ?: return@matchMatcher + // Use GSON to reflect the JSON into a NeuPetSkinJson object + val petItemData = Gson().fromJson(jsonObject, NeuPetSkinJson::class.java) + + petSkins.getOrPut(petName) { mutableMapOf() }[rawInternalName] = petItemData + } + } + } + + @HandleEvent + fun onCommandRegistration(event: CommandRegistrationEvent) { + event.register("shpetxp") { + description = "Calculates the pet xp from a given level and rarity." + category = CommandCategory.DEVELOPER_TEST + callback { levelToXPCommand(it) } + } + } + + fun PetData.getSkinOrNull(): NeuPetSkinJson? { + if (skinSymbolColor == null && skinInternalName == null) return null + + val cleanPetName = cleanName ?: return null + val possiblePetSkins = petSkins[cleanPetName] ?: return null + if (possiblePetSkins.size == 1) return possiblePetSkins.values.first() + + skinInternalName?.let { return possiblePetSkins[it.asString()] } + + val possibleSymbolSkins = possiblePetSkins.filter { + val cosmeticRarity = it.value.rarity ?: return@filter false + cosmeticRarity.color == skinSymbolColor + } + + return possibleSymbolSkins.values.firstOrNull() + } +} diff --git a/src/main/java/at/hannibal2/skyhanni/utils/RegexUtils.kt b/src/main/java/at/hannibal2/skyhanni/utils/RegexUtils.kt index 4980f0d37420..35700ccc2d3e 100644 --- a/src/main/java/at/hannibal2/skyhanni/utils/RegexUtils.kt +++ b/src/main/java/at/hannibal2/skyhanni/utils/RegexUtils.kt @@ -55,6 +55,9 @@ object RegexUtils { fun Pattern.anyMatches(list: Sequence?): Boolean = anyMatches(list?.toList()) fun Pattern.matchGroup(text: String, groupName: String): String? = matchMatcher(text) { groupOrNull(groupName) } + fun Pattern.firstMatchGroup(list: List, groupName: String): String? = firstMatcher(list) { + groupOrNull(groupName) + } fun Pattern.matchGroups(text: String, vararg groups: String): List? = matchMatcher(text) { groups.toList().map { groupOrNull(it) } } diff --git a/src/main/java/at/hannibal2/skyhanni/utils/RenderUtils.kt b/src/main/java/at/hannibal2/skyhanni/utils/RenderUtils.kt index b389e6c80500..c5365a0dfccf 100644 --- a/src/main/java/at/hannibal2/skyhanni/utils/RenderUtils.kt +++ b/src/main/java/at/hannibal2/skyhanni/utils/RenderUtils.kt @@ -13,11 +13,13 @@ import at.hannibal2.skyhanni.features.misc.PatcherFixes import at.hannibal2.skyhanni.features.misc.RoundedRectangleOutlineShader import at.hannibal2.skyhanni.features.misc.RoundedRectangleShader import at.hannibal2.skyhanni.features.misc.RoundedTextureShader +import at.hannibal2.skyhanni.shader.CircleShader import at.hannibal2.skyhanni.utils.CollectionUtils.zipWithNext3 import at.hannibal2.skyhanni.utils.ColorUtils.getFirstColorCode import at.hannibal2.skyhanni.utils.LocationUtils.calculateEdges import at.hannibal2.skyhanni.utils.LorenzColor.Companion.toLorenzColor import at.hannibal2.skyhanni.utils.LorenzUtils.getCorners +import at.hannibal2.skyhanni.utils.RenderUtils.drawRoundRect import at.hannibal2.skyhanni.utils.compat.GuiScreenUtils import at.hannibal2.skyhanni.utils.renderables.Renderable import at.hannibal2.skyhanni.utils.renderables.RenderableUtils.renderXAligned @@ -1934,6 +1936,43 @@ object RenderUtils { GlStateManager.popMatrix() } + /** + * Method to draw a circle. + * + * **NOTE:** If you are using [GlStateManager.translate] or [GlStateManager.scale] + * with this method, ensure they are invoked in the correct order if you use both. That is, [GlStateManager.translate] + * is called **BEFORE** [GlStateManager.scale], otherwise the rectangle will not be rendered correctly + * + * @param x The x-coordinate of the circle's center. + * @param y The y-coordinate of the circle's center. + * @param radius The circle's radius. + * @param color The fill color. + * @param angle1 defines the start of the semicircle (Default value makes it a full circle). Must be in range [0,2*pi] (0 is on the left and increases counterclockwise) + * @param angle2 defines the end of the semicircle (Default value makes it a full circle). Must be in range [0,2*pi] (0 is on the left and increases counterclockwise) + * @param smoothness smooths out the edge. (In amount of blurred pixels) + */ + fun drawFilledCircle(x: Int, y: Int, radius: Int, color: Color, smoothness: Float = 2.5f, angle1: Float = 7.0f, angle2: Float = 7.0f) { + val scaleFactor = ScaledResolution(Minecraft.getMinecraft()).scaleFactor + val radiusIn = radius * scaleFactor + val xIn = x * scaleFactor + val yIn = y * scaleFactor + + CircleShader.scaleFactor = scaleFactor.toFloat() + CircleShader.radius = radiusIn.toFloat() + CircleShader.smoothness = smoothness.toFloat() + CircleShader.centerPos = floatArrayOf((xIn + radiusIn).toFloat(), (yIn + radiusIn).toFloat()) + CircleShader.angle1 = angle1 - Math.PI.toFloat() + CircleShader.angle2 = angle2 - Math.PI.toFloat() + + GlStateManager.pushMatrix() + ShaderManager.enableShader(ShaderManager.Shaders.CIRCLE) + + Gui.drawRect(x - 5, y - 5, x + radius * 2 + 5, y + radius * 2 + 5, color.rgb) + + ShaderManager.disableShader() + GlStateManager.popMatrix() + } + fun getAlpha(): Float { colorBuffer.clear() GlStateManager.getFloat(GL11.GL_CURRENT_COLOR, colorBuffer) diff --git a/src/main/java/at/hannibal2/skyhanni/utils/SkyBlockItemModifierUtils.kt b/src/main/java/at/hannibal2/skyhanni/utils/SkyBlockItemModifierUtils.kt index a0ab9dd7fb40..6da913dc2e94 100644 --- a/src/main/java/at/hannibal2/skyhanni/utils/SkyBlockItemModifierUtils.kt +++ b/src/main/java/at/hannibal2/skyhanni/utils/SkyBlockItemModifierUtils.kt @@ -1,7 +1,6 @@ package at.hannibal2.skyhanni.utils import at.hannibal2.skyhanni.config.ConfigManager -import at.hannibal2.skyhanni.data.PetApi import at.hannibal2.skyhanni.mixins.hooks.ItemStackCachedData import at.hannibal2.skyhanni.utils.ItemUtils.extraAttributes import at.hannibal2.skyhanni.utils.ItemUtils.getInternalName @@ -10,7 +9,9 @@ import at.hannibal2.skyhanni.utils.ItemUtils.getStringList import at.hannibal2.skyhanni.utils.ItemUtils.name import at.hannibal2.skyhanni.utils.NeuInternalName.Companion.toInternalName import at.hannibal2.skyhanni.utils.NumberUtil.isPositive +import at.hannibal2.skyhanni.utils.PetUtils.petItemNamePattern import at.hannibal2.skyhanni.utils.RegexUtils.anyMatches +import at.hannibal2.skyhanni.utils.RegexUtils.matchGroup import at.hannibal2.skyhanni.utils.StringUtils.removeColor import com.google.gson.JsonObject import net.minecraft.item.Item @@ -101,9 +102,9 @@ object SkyBlockItemModifierUtils { ConfigManager.gson.fromJson(getExtraAttributes()?.getString("petInfo"), JsonObject::class.java) @Suppress("CAST_NEVER_SUCCEEDS") - inline val ItemStack.cachedData get() = (this as ItemStackCachedData).skyhanni_cachedData + inline val ItemStack.cachedData: CachedItemData get() = (this as ItemStackCachedData).skyhanni_cachedData - fun ItemStack.getPetLevel(): Int = PetApi.getPetLevel(displayName) ?: 0 + fun ItemStack.getPetLevel(): Int = petItemNamePattern.matchGroup(displayName, "level")?.toInt() ?: 0 fun ItemStack.getMaxPetLevel() = if (this.getInternalName() == "GOLDEN_DRAGON;4".toInternalName()) 200 else 100 diff --git a/src/main/java/at/hannibal2/skyhanni/utils/StringUtils.kt b/src/main/java/at/hannibal2/skyhanni/utils/StringUtils.kt index e51e280fc890..4cd7321d0792 100644 --- a/src/main/java/at/hannibal2/skyhanni/utils/StringUtils.kt +++ b/src/main/java/at/hannibal2/skyhanni/utils/StringUtils.kt @@ -17,6 +17,7 @@ import net.minecraft.util.ChatStyle import net.minecraft.util.EnumChatFormatting import net.minecraft.util.IChatComponent import java.util.Base64 +import java.util.Locale import java.util.NavigableMap import java.util.UUID import java.util.function.Predicate @@ -41,11 +42,8 @@ object StringUtils { fun String.removeNonAscii(): String = asciiPattern.matcher(this).replaceAll("") fun String.firstLetterUppercase(): String { - if (isEmpty()) return this - - val lowercase = lowercase() - val first = lowercase[0].uppercase() - return first + lowercase.substring(1) + return this.lowercase(Locale.getDefault()) + .replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } } private val formattingChars = "kmolnrKMOLNR".toSet() @@ -328,6 +326,7 @@ object StringUtils { } fun String.convertToFormatted(): String = this.replace("&&", "§") + fun String.convertToUnformatted(): String = this.replace("§", "&") fun String.allLettersFirstUppercase() = split("_").joinToString(" ") { it.firstLetterUppercase() } diff --git a/src/main/java/at/hannibal2/skyhanni/utils/renderables/Renderable.kt b/src/main/java/at/hannibal2/skyhanni/utils/renderables/Renderable.kt index 3a8bc7c48bc2..e8ce91670059 100644 --- a/src/main/java/at/hannibal2/skyhanni/utils/renderables/Renderable.kt +++ b/src/main/java/at/hannibal2/skyhanni/utils/renderables/Renderable.kt @@ -26,6 +26,7 @@ import at.hannibal2.skyhanni.utils.NeuItems.renderOnScreen import at.hannibal2.skyhanni.utils.RenderUtils import at.hannibal2.skyhanni.utils.RenderUtils.HorizontalAlignment import at.hannibal2.skyhanni.utils.RenderUtils.VerticalAlignment +import at.hannibal2.skyhanni.utils.RenderUtils.drawFilledCircle import at.hannibal2.skyhanni.utils.compat.getTooltipCompat import at.hannibal2.skyhanni.utils.guide.GuideGUI import at.hannibal2.skyhanni.utils.renderables.Renderable.Companion.shouldAllowLink @@ -1688,4 +1689,46 @@ interface Renderable { } } } + + class CircularRenderable( + private val backgroundColor: Color, + private val radius: Int, + private val border: CircularRenderable? = null, + private val itemStack: ItemStack? = null, + private val itemScale: Double = 1.9, + private val filledPercentage: Double = 100.0, + private val unfilledColor: Color = Color.LIGHT_GRAY, + ) : Renderable { + private val totalRadius: Int = max(radius, border?.totalRadius ?: 0) + private val diffRadius: Int = totalRadius - radius + + override val width: Int = totalRadius * 2 + override val height: Int = totalRadius * 2 + override val horizontalAlign = HorizontalAlignment.LEFT + override val verticalAlign = VerticalAlignment.TOP + + override fun render(posX: Int, posY: Int) { + border?.render(posX, posY) + + if (filledPercentage < 100.0) { + val baseAngle = Math.PI.toFloat() * 3f / 2f + val endAngle = (baseAngle + ((100.0 - filledPercentage) / 50.0 * Math.PI).toFloat()).mod(2f * Math.PI.toFloat()) + drawFilledCircle(diffRadius, diffRadius, radius, backgroundColor, angle1 = baseAngle, angle2 = endAngle) + drawFilledCircle(diffRadius, diffRadius, radius, unfilledColor, angle1 = endAngle, angle2 = baseAngle) + } else { + drawFilledCircle(diffRadius, diffRadius, radius, backgroundColor) + } + + + itemStack?.let { stack -> + val itemWidth = (16 * itemScale).toInt() + val itemHeight = (16 * itemScale).toInt() + val itemX = totalRadius - itemWidth / 2 + val itemY = totalRadius - itemHeight / 2 + + stack.renderOnScreen(itemX.toFloat(), itemY.toFloat(), scaleMultiplier = itemScale, rescaleSkulls = true) + } + } + + } } diff --git a/src/main/java/at/hannibal2/skyhanni/utils/shader/ShaderManager.kt b/src/main/java/at/hannibal2/skyhanni/utils/shader/ShaderManager.kt index ed4dbabdfe6c..b9a897b8ed64 100644 --- a/src/main/java/at/hannibal2/skyhanni/utils/shader/ShaderManager.kt +++ b/src/main/java/at/hannibal2/skyhanni/utils/shader/ShaderManager.kt @@ -6,6 +6,7 @@ import at.hannibal2.skyhanni.features.misc.DarkenShader import at.hannibal2.skyhanni.features.misc.RoundedRectangleOutlineShader import at.hannibal2.skyhanni.features.misc.RoundedRectangleShader import at.hannibal2.skyhanni.features.misc.RoundedTextureShader +import at.hannibal2.skyhanni.shader.CircleShader import at.hannibal2.skyhanni.test.command.ErrorManager import at.hannibal2.skyhanni.utils.LorenzUtils import net.minecraft.client.Minecraft @@ -30,7 +31,8 @@ object ShaderManager { ROUNDED_RECTANGLE(RoundedRectangleShader.INSTANCE), ROUNDED_RECT_OUTLINE(RoundedRectangleOutlineShader.INSTANCE), ROUNDED_TEXTURE(RoundedTextureShader.INSTANCE), - DARKEN(DarkenShader.INSTANCE) + DARKEN(DarkenShader.INSTANCE), + CIRCLE(CircleShader.INSTANCE), ; fun enableShader() = enableShader(this) diff --git a/src/main/resources/assets/skyhanni/shaders/circle.fsh b/src/main/resources/assets/skyhanni/shaders/circle.fsh new file mode 100644 index 000000000000..64f9029a679f --- /dev/null +++ b/src/main/resources/assets/skyhanni/shaders/circle.fsh @@ -0,0 +1,43 @@ +#version 120 + +const float pi = 3.1415926535897932384626433832795028841971693993751058209749445923078164062f; + +uniform float scaleFactor; +uniform float radius; +uniform float smoothness; +uniform vec2 centerPos; +uniform float angle1; +uniform float angle2; + +varying vec4 color; + +void main() { + float xScale = gl_ModelViewMatrix[0][0]; + float yScale = gl_ModelViewMatrix[1][1]; + float xTranslation = gl_ModelViewMatrix[3][0]; + float yTranslation = gl_ModelViewMatrix[3][1]; + + vec2 cords = vec2(gl_FragCoord.x, gl_FragCoord.y); + + vec2 newCenterPos = vec2((centerPos.x + (radius * (xScale - 1.0))) + (xTranslation * scaleFactor), (centerPos.y - (radius * (yScale - 1.0))) - (yTranslation * scaleFactor)); + + float newRadius = radius * min(xScale,yScale); + + vec2 adjusted = cords - newCenterPos; + + float smoothed = 1.0 - smoothstep(pow(newRadius - smoothness,2.0), pow(newRadius,2.0), pow(adjusted.x, 2.0) + pow(adjusted.y, 2.0)); + + float current = atan(adjusted.y,adjusted.x); + + float sanity = step(angle1,angle2); + + float lim1 = step(current,angle1); + float lim2 = step(angle2,current); + + float lim3 = step(angle1,current); + float lim4 = step(current,angle2); + + float lim = max(lim1,lim2)*sanity+(1.0-sanity)*(1.0-max(lim3,lim4)); + + gl_FragColor = color * vec4(1.0, 1.0, 1.0, smoothed*lim); +} diff --git a/src/main/resources/assets/skyhanni/shaders/circle.vsh b/src/main/resources/assets/skyhanni/shaders/circle.vsh new file mode 100644 index 000000000000..acfb526629fd --- /dev/null +++ b/src/main/resources/assets/skyhanni/shaders/circle.vsh @@ -0,0 +1,8 @@ +#version 120 + +varying vec4 color; + +void main() { + gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex; + color = gl_Color; +}