aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/at/hannibal2/skyhanni/features/misc
diff options
context:
space:
mode:
authorNetheriteMiner <88792142+NetheriteMiner@users.noreply.github.com>2023-04-27 06:07:33 -0400
committerGitHub <noreply@github.com>2023-04-27 12:07:33 +0200
commit2dd1970d6d31ea1e2bbfa30b57141ea9a4720834 (patch)
tree4d5879b3eaecb851485436c3a266e3d5814d6d06 /src/main/java/at/hannibal2/skyhanni/features/misc
parentd889efc0ca80458442df14500b71092008bb95b2 (diff)
downloadskyhanni-2dd1970d6d31ea1e2bbfa30b57141ea9a4720834.tar.gz
skyhanni-2dd1970d6d31ea1e2bbfa30b57141ea9a4720834.tar.bz2
skyhanni-2dd1970d6d31ea1e2bbfa30b57141ea9a4720834.zip
Added Discord RPC (#35)
Co-authored-by: hannibal2 <24389977+hannibal00212@users.noreply.github.com>
Diffstat (limited to 'src/main/java/at/hannibal2/skyhanni/features/misc')
-rw-r--r--src/main/java/at/hannibal2/skyhanni/features/misc/discordrpc/DiscordLocationKey.kt112
-rw-r--r--src/main/java/at/hannibal2/skyhanni/features/misc/discordrpc/DiscordRPCManager.kt183
-rw-r--r--src/main/java/at/hannibal2/skyhanni/features/misc/discordrpc/DiscordStatus.kt149
3 files changed, 444 insertions, 0 deletions
diff --git a/src/main/java/at/hannibal2/skyhanni/features/misc/discordrpc/DiscordLocationKey.kt b/src/main/java/at/hannibal2/skyhanni/features/misc/discordrpc/DiscordLocationKey.kt
new file mode 100644
index 000000000..8fbfadbd0
--- /dev/null
+++ b/src/main/java/at/hannibal2/skyhanni/features/misc/discordrpc/DiscordLocationKey.kt
@@ -0,0 +1,112 @@
+package at.hannibal2.skyhanni.features.misc.discordrpc
+
+class DiscordLocationKey {
+
+ private val normalRPC = setOf(
+ "auction-house",
+ "bank",
+ "canvas-room",
+ "coal-mine",
+ "colosseum",
+ "farm",
+ "fashion-shop",
+ "flower-house",
+ "forest",
+ "graveyard",
+ "library",
+ "mountain",
+ "ruins",
+ "tavern",
+ "village",
+ "wilderness",
+ "wizard-tower",
+ "birch-park",
+ "spruce-woods",
+ "savanna-woodland",
+ "dark-thicket",
+ "jungle-island",
+ "gold-mine",
+ "slimehill",
+ "diamond-reserve",
+ "obsidian-sanctuary",
+ "the-barn",
+ "mushroom-desert",
+ "the-end"
+ )
+ // list of tokens where the name can just be lowercased and spaces can be replaced with dashes
+
+ private val specialRPC = mapOf(
+ "Fisherman's Hut" to "fishermans-hut", "Unincorporated" to "high-level",
+ "Dragon's Nest" to "dragons-nest", "Void Sepulture" to "the-end", "Void Slate" to "the-end",
+ "Zealot Bruiser Hideout" to "the-end", "Desert Settlement" to "mushroom-desert",
+ "Oasis" to "mushroom-desert", "Desert Mountain" to "mushroom-desert", "Jake's House" to "mushroom-desert",
+ "Trapper's Den" to "mushroom-desert", "Mushroom Gorge" to "mushroom-desert",
+ "Glowing Mushroom Cave" to "mushroom-desert", "Overgrown Mushroom Cave" to "mushroom-desert",
+ "Shepherd's Keep" to "mushroom-desert", "Treasure Hunter Camp" to "mushroom-desert",
+ "Windmill" to "the-barn", "Spider's Den" to "spiders-den", "Arachne's Burrow" to "spiders-den",
+ "Arachne's Sanctuary" to "spiders-den", "Archaeologist's Camp" to "spiders-den",
+ "Grandma's House" to "spiders-den", "Gravel Mines" to "spiders-den", "Spider Mound" to "spiders-den",
+ "Melody's Plateau" to "forest", "Viking Longhouse" to "forest", "Lonely Island" to "forest",
+ "Howling Cave" to "forest"
+ ) // maps locations that do have a token, but have parentheses or a legacy key
+
+ private val specialNetherRPC = arrayOf(
+ "Aura's Lab",
+ "Barbarian Outpost",
+ "Belly of the Beast",
+ "Blazing Volcano",
+ "Burning Desert",
+ "Cathedral",
+ "Chief's Hut",
+ "Courtyard",
+ "Crimson Fields",
+ "Crimson Isle",
+ "Dojo",
+ "Dragontail Auction House",
+ "Dragontail Bank",
+ "Dragontail Bazaar",
+ "Dragontail Blacksmith",
+ "Dragontail Townsquare",
+ "Dragontail",
+ "Forgotten Skull",
+ "Igrupan's Chicken Coop",
+ "Igrupan's House",
+ "Mage Council",
+ "Mage Outpost",
+ "Magma Chamber",
+ "Matriarch's Lair",
+ "Minion Shop",
+ "Mystic Marsh",
+ "Odger's Hut",
+ "Plhlegblast Pool",
+ "Ruins of Ashfang",
+ "Scarleton Auction House",
+ "Scarleton Bank",
+ "Scarleton Bazaar",
+ "Scarleton Blacksmith",
+ "Scarleton Minion Shop",
+ "Scarleton Plaza",
+ "Scarleton",
+ "Smoldering Tomb",
+ "Stronghold",
+ "The Bastion",
+ "The Dukedom",
+ "The Wasteland",
+ "Throne Room"
+ )
+ // list of nether locations because there are sooo many (truncated some according to scoreboard)
+
+ fun getDiscordIconKey(location: String): String {
+ val keyIfNormal = location.lowercase().replace(' ', '-')
+
+ return if (normalRPC.contains(keyIfNormal)) {
+ keyIfNormal
+ } else if (specialRPC.containsKey(location)) {
+ specialRPC[location]!!
+ } else if (specialNetherRPC.contains(location)) {
+ "blazing-fortress"
+ } else {
+ "skyblock" // future proofing since we can't update the images anymore :(
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/java/at/hannibal2/skyhanni/features/misc/discordrpc/DiscordRPCManager.kt b/src/main/java/at/hannibal2/skyhanni/features/misc/discordrpc/DiscordRPCManager.kt
new file mode 100644
index 000000000..c5dfcdff2
--- /dev/null
+++ b/src/main/java/at/hannibal2/skyhanni/features/misc/discordrpc/DiscordRPCManager.kt
@@ -0,0 +1,183 @@
+package at.hannibal2.skyhanni.features.misc.discordrpc
+
+// This entire file was taken from SkyblockAddons code, ported to SkyHanni
+
+import at.hannibal2.skyhanni.SkyHanniMod.*
+import at.hannibal2.skyhanni.events.ConfigLoadEvent
+import at.hannibal2.skyhanni.utils.LorenzUtils
+import com.google.gson.JsonObject
+import com.jagrosh.discordipc.IPCClient
+import com.jagrosh.discordipc.IPCListener
+import com.jagrosh.discordipc.entities.RichPresence
+import io.github.moulberry.moulconfig.observer.Property
+import kotlinx.coroutines.launch
+import net.minecraftforge.event.world.WorldEvent
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
+import net.minecraftforge.fml.common.gameevent.TickEvent
+import net.minecraftforge.fml.common.network.FMLNetworkEvent
+import java.util.*
+import java.util.concurrent.Executors
+import java.util.concurrent.ScheduledExecutorService
+import java.util.concurrent.TimeUnit
+
+class DiscordRPCManager : IPCListener {
+ private val applicationID = 653443797182578707L
+ private val updatePeriod = 4200L
+
+ private val config get() = feature.misc.discordRPC
+
+ private var client: IPCClient? = null
+ private lateinit var secondLine: DiscordStatus
+ private lateinit var firstLine: DiscordStatus
+ private var startTimestamp: Long? = null
+ private var startOnce = false
+
+ private var updateTimer: Timer? = null
+ private var connected = false
+
+ private val DiscordLocationKey = DiscordLocationKey()
+
+ private fun start() {
+ coroutineScope.launch {
+ try {
+ if (isActive()) {
+ return@launch
+ }
+ consoleLog("Starting Discord RPC...")
+
+ firstLine = getStatusByConfigId(config.firstLine.get())
+ secondLine = getStatusByConfigId(config.secondLine.get())
+ startTimestamp = System.currentTimeMillis()
+ client = IPCClient(applicationID)
+ client?.setListener(this@DiscordRPCManager) // why must kotlin be this way
+
+ try {
+ client?.connect()
+ } catch (ex: Exception) {
+ consoleLog("Warn: Failed to connect to RPC!")
+ consoleLog(ex.toString())
+ }
+ } catch (ex: Throwable) {
+ consoleLog("Warn: Discord RPC has thrown an unexpected error while trying to start...")
+ consoleLog(ex.toString())
+ }
+ }
+ }
+
+ private fun stop() {
+ coroutineScope.launch {
+ if (isActive()) {
+ connected = false
+ client?.close()
+ startOnce = false
+ }
+ }
+ }
+
+ private fun isActive() = client != null && connected
+
+ @SubscribeEvent
+ fun onConfigLoad(event: ConfigLoadEvent) {
+ for (property in listOf(
+ config.firstLine,
+ config.secondLine,
+ config.customText,
+ )) {
+ property.whenChangedWithDifference {
+ if (isActive()) {
+ updatePresence()
+ }
+ }
+ }
+ config.enabled.whenChanged { _, new ->
+ if (new) {
+// start()
+ } else {
+ stop()
+ }
+ }
+ }
+
+ fun Property<*>.whenChangedWithDifference(run: () -> (Unit)) {
+ whenChanged { old, new -> if (old != new) run() }
+ }
+
+ fun updatePresence() {
+ val location = LorenzUtils.skyBlockArea
+ val discordIconKey = DiscordLocationKey.getDiscordIconKey(location)
+
+ secondLine = getStatusByConfigId(config.secondLine.get())
+ firstLine = getStatusByConfigId(config.firstLine.get())
+ val presence: RichPresence = RichPresence.Builder()
+ .setDetails(firstLine.getDisplayString())
+ .setState(secondLine.getDisplayString())
+ .setStartTimestamp(startTimestamp!!)
+ .setLargeImage(discordIconKey, location)
+ .build()
+ client?.sendRichPresence(presence)
+ }
+
+ override fun onReady(client: IPCClient) {
+ consoleLog("Discord RPC Started.")
+ connected = true
+ updateTimer = Timer()
+ updateTimer?.schedule(object : TimerTask() {
+ override fun run() {
+ updatePresence()
+ }
+ }, 0, updatePeriod)
+ }
+
+ override fun onClose(client: IPCClient, json: JsonObject) {
+ consoleLog("Discord RPC closed.")
+ this.client = null
+ connected = false
+ cancelTimer()
+ }
+
+ override fun onDisconnect(client: IPCClient?, t: Throwable?) {
+ consoleLog("Discord RPC disconnected.")
+ this.client = null
+ connected = false
+ cancelTimer()
+ }
+
+ private fun cancelTimer() {
+ updateTimer?.let {
+ it.cancel()
+ updateTimer = null
+ }
+ }
+
+ private fun getStatusByConfigId(id: Int) = DiscordStatus.values().getOrElse(id) { DiscordStatus.NONE }
+
+ private fun isEnabled() = config.enabled.get()
+
+ @SubscribeEvent
+ fun onTick(event: TickEvent.ClientTickEvent) {
+ if (startOnce || !isEnabled()) return // the mod has already started the connection process. this variable is my way of running a function when the player joins skyblock but only running it again once they join and leave.
+ if (LorenzUtils.inSkyBlock) {
+ start()
+ startOnce = true
+ }
+ }
+
+ @SubscribeEvent
+ fun onWorldChange(event: WorldEvent.Load) {
+ val executor: ScheduledExecutorService = Executors.newSingleThreadScheduledExecutor()
+ executor.schedule(
+ {
+ if (!LorenzUtils.inSkyBlock) {
+ stop()
+ }
+ },
+ 5,
+ TimeUnit.SECONDS
+ ) // wait 5 seconds to check if the new world is skyblock or not before stopping the function
+ }
+
+ @SubscribeEvent
+ fun onDisconnect(event: FMLNetworkEvent.ClientDisconnectionFromServerEvent) {
+ stop()
+ }
+}
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
new file mode 100644
index 000000000..ac73c33cf
--- /dev/null
+++ b/src/main/java/at/hannibal2/skyhanni/features/misc/discordrpc/DiscordStatus.kt
@@ -0,0 +1,149 @@
+package at.hannibal2.skyhanni.features.misc.discordrpc
+
+// SkyblockAddons code, adapted for SkyHanni
+
+import at.hannibal2.skyhanni.SkyHanniMod
+import at.hannibal2.skyhanni.data.ActionBarStatsData
+import at.hannibal2.skyhanni.data.HypixelData
+import at.hannibal2.skyhanni.data.ScoreboardData
+import at.hannibal2.skyhanni.utils.LorenzUtils
+import at.hannibal2.skyhanni.utils.StringUtils.firstLetterUppercase
+import at.hannibal2.skyhanni.utils.StringUtils.removeColor
+import io.github.moulberry.notenoughupdates.util.SkyBlockTime
+import java.util.function.Supplier
+import java.util.regex.Pattern
+
+enum class DiscordStatus(private val displayMessageSupplier: Supplier<String>?) {
+ // implements "ButtonSelect:SelectItem". no idea how to translate that into skyhanni
+
+ NONE(null),
+
+ LOCATION({
+ val location = LorenzUtils.skyBlockArea
+ if (location == "Your Island") {
+ "Private Island"
+ } else {
+ location
+ /**
+ * looks slightly weird if visiting someone else's island,
+ * I was thinking of using LorenzUtils . skyblockIsland to determine if they're visiting,
+ * but it takes too long to load, so we 'd have to put in some sort of artificial delay
+ * like what I did in DiscordRPCManager.onWorldChange.
+ * after that, use the tab-list "Owner:" line to get the person we're visiting, but I don't know if
+ * that'll work with coops, and you'd have to deal with color codes as well
+ * as again, I'm pretty sure sba had "'s Island" without the name filled in this entire time,
+ * so I 'd rather have [RANK] NameThatGetsCutOff for example than 's Island
+ */
+ }
+ }),
+
+ PURSE({
+ val scoreboard = ScoreboardData.sidebarLinesFormatted
+ var coins = ""
+
+ for (line in scoreboard) {
+ if (line.startsWith("Purse: ") || line.startsWith("Piggy: ")) {
+ coins = line.subSequence(9 until line.length).toString()
+ }
+ }
+
+ if (coins == "1") "1 Coin" else "$coins Coins"
+ }),
+
+ BITS({
+ var bits = ""
+ for (line in ScoreboardData.sidebarLinesFormatted) {
+ if (line.startsWith("Bits: ")) {
+ bits = line.subSequence(8 until line.length).toString()
+ }
+ }
+
+ when (bits) {
+ "1" -> "1 Bit"
+ "" -> "0 Bits"
+ else -> "$bits Bits"
+ }
+ }),
+
+ STATS({
+ val groups = ActionBarStatsData.groups
+ var statString = ""
+ for (item in groups.indices) {
+ when (groups[item]) {
+ "❤" -> statString = "❤${groups[item - 1]} "
+ "❈ Defense" -> statString = "$statString❈${groups[item - 1]} "
+ "✎ Mana" -> statString = "$statString✎${groups[item - 1]} "
+ }
+ }
+ statString
+ }),
+
+ ITEM({
+ val player: net.minecraft.client.entity.EntityPlayerSP = net.minecraft.client.Minecraft.getMinecraft().thePlayer
+ if (player.heldItem != null) {
+ String.format("Holding ${player.heldItem.displayName.removeColor()}")
+ } else {
+ "No item in hand"
+ }
+ }),
+
+ TIME({
+ fun formatNum(num: Int): Int {
+ val rem = num % 10
+ var returnNum = num - rem // floor()
+ if (returnNum == 0) {
+ returnNum = "0$num".toInt()
+ /**
+ * and this is so that if the minute value is ever
+ * a single digit (0 after being floored), it displays as 00 because 12:0pm just looks bad
+ */
+ }
+ return returnNum
+ }
+
+ val date: SkyBlockTime = SkyBlockTime.now()
+ val hour = if (date.hour > 12) date.hour - 12 else date.hour
+ val timeOfDay = if (date.hour > 11) "pm" else "am" // hooray for 12-hour clocks
+ "${SkyBlockTime.monthName(date.month)} ${date.day}${SkyBlockTime.daySuffix(date.day)}, $hour:${formatNum(date.minute)}$timeOfDay" // Early Winter 1st, 12:00pm
+ }),
+
+ PROFILE({
+ HypixelData.profileName.firstLetterUppercase()
+ }),
+
+ SLAYER({
+ var slayerName = ""
+ var slayerLevel = ""
+ var bossAlive = "spawning"
+ val slayerRegex =
+ Pattern.compile("((?:\\w| )*) ([IV]+)") // Samples: Revenant Horror I; Tarantula Broodfather IV
+
+ for (line in ScoreboardData.sidebarLinesFormatted) {
+ val noColorLine = line.removeColor()
+ val match = slayerRegex.matcher(noColorLine)
+ if (match.matches()) {
+ slayerName = match.group(1)
+ slayerLevel = match.group(2)
+ } else if (noColorLine == "Slay the boss!") bossAlive = "slaying"
+ else if (noColorLine == "Boss slain!") bossAlive = "slain"
+ }
+
+ if (slayerLevel == "") "Planning to do a slayer quest"// selected slayer in rpc but hasn't started a quest
+ else if (bossAlive == "spawning") "Spawning a $slayerName $slayerLevel boss."
+ else if (bossAlive == "slaying") "Slaying a $slayerName $slayerLevel boss."
+ else if (bossAlive == "slain") "Finished slaying a $slayerName $slayerLevel boss."
+ else "Something went wrong with slayer detection!"
+ }),
+
+ CUSTOM({
+ SkyHanniMod.feature.misc.discordRPC.customText.get() // custom field in the config
+ })
+ ;
+
+ fun getDisplayString(): String {
+ if (displayMessageSupplier != null) {
+ return displayMessageSupplier.get()
+ }
+ return ""
+ }
+} \ No newline at end of file