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