aboutsummaryrefslogtreecommitdiff
path: root/src/main/kotlin/apis
diff options
context:
space:
mode:
authorLinnea Gräf <nea@nea.moe>2024-08-28 19:04:24 +0200
committerLinnea Gräf <nea@nea.moe>2024-08-28 19:04:24 +0200
commitd2f240ff0ca0d27f417f837e706c781a98c31311 (patch)
tree0db7aff6cc14deaf36eed83889d59fd6b3a6f599 /src/main/kotlin/apis
parenta6906308163aa3b2d18fa1dc1aa71ac9bbcc83ab (diff)
downloadfirmament-d2f240ff0ca0d27f417f837e706c781a98c31311.tar.gz
firmament-d2f240ff0ca0d27f417f837e706c781a98c31311.tar.bz2
firmament-d2f240ff0ca0d27f417f837e706c781a98c31311.zip
Refactor source layout
Introduce compat source sets and move all kotlin sources to the main directory [no changelog]
Diffstat (limited to 'src/main/kotlin/apis')
-rw-r--r--src/main/kotlin/apis/Profiles.kt194
-rw-r--r--src/main/kotlin/apis/Routes.kt95
-rw-r--r--src/main/kotlin/apis/UrsaManager.kt72
3 files changed, 361 insertions, 0 deletions
diff --git a/src/main/kotlin/apis/Profiles.kt b/src/main/kotlin/apis/Profiles.kt
new file mode 100644
index 0000000..789364a
--- /dev/null
+++ b/src/main/kotlin/apis/Profiles.kt
@@ -0,0 +1,194 @@
+
+
+@file:UseSerializers(DashlessUUIDSerializer::class, InstantAsLongSerializer::class)
+
+package moe.nea.firmament.apis
+
+import io.github.moulberry.repo.constants.Leveling
+import io.github.moulberry.repo.data.Rarity
+import kotlinx.datetime.Instant
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.UseSerializers
+import moe.nea.firmament.repo.RepoManager
+import moe.nea.firmament.util.LegacyFormattingCode
+import moe.nea.firmament.util.SkyblockId
+import moe.nea.firmament.util.assertNotNullOr
+import moe.nea.firmament.util.json.DashlessUUIDSerializer
+import moe.nea.firmament.util.json.InstantAsLongSerializer
+import net.minecraft.util.DyeColor
+import net.minecraft.util.Formatting
+import java.util.*
+import kotlin.reflect.KProperty1
+
+
+@Serializable
+data class CollectionSkillData(
+ val items: Map<CollectionType, CollectionInfo>
+)
+
+@Serializable
+data class CollectionResponse(
+ val success: Boolean,
+ val collections: Map<String, CollectionSkillData>
+)
+
+@Serializable
+data class CollectionInfo(
+ val name: String,
+ val maxTiers: Int,
+ val tiers: List<CollectionTier>
+)
+
+@Serializable
+data class CollectionTier(
+ val tier: Int,
+ val amountRequired: Long,
+ val unlocks: List<String>,
+)
+
+
+@Serializable
+data class Profiles(
+ val success: Boolean,
+ val profiles: List<Profile>?
+)
+
+@Serializable
+data class Profile(
+ @SerialName("profile_id")
+ val profileId: UUID,
+ @SerialName("cute_name")
+ val cuteName: String,
+ val selected: Boolean = false,
+ val members: Map<UUID, Member>,
+)
+
+enum class Skill(val accessor: KProperty1<Member, Double>, val color: DyeColor, val icon: SkyblockId) {
+ FARMING(Member::experienceSkillFarming, DyeColor.YELLOW, SkyblockId("ROOKIE_HOE")),
+ FORAGING(Member::experienceSkillForaging, DyeColor.BROWN, SkyblockId("TREECAPITATOR_AXE")),
+ MINING(Member::experienceSkillMining, DyeColor.LIGHT_GRAY, SkyblockId("DIAMOND_PICKAXE")),
+ ALCHEMY(Member::experienceSkillAlchemy, DyeColor.PURPLE, SkyblockId("BREWING_STAND")),
+ TAMING(Member::experienceSkillTaming, DyeColor.GREEN, SkyblockId("SUPER_EGG")),
+ FISHING(Member::experienceSkillFishing, DyeColor.BLUE, SkyblockId("FARMER_ROD")),
+ RUNECRAFTING(Member::experienceSkillRunecrafting, DyeColor.PINK, SkyblockId("MUSIC_RUNE;1")),
+ CARPENTRY(Member::experienceSkillCarpentry, DyeColor.ORANGE, SkyblockId("WORKBENCH")),
+ COMBAT(Member::experienceSkillCombat, DyeColor.RED, SkyblockId("UNDEAD_SWORD")),
+ SOCIAL(Member::experienceSkillSocial, DyeColor.WHITE, SkyblockId("EGG_HUNT")),
+ ENCHANTING(Member::experienceSkillEnchanting, DyeColor.MAGENTA, SkyblockId("ENCHANTMENT_TABLE")),
+ ;
+
+ fun getMaximumLevel(leveling: Leveling) = assertNotNullOr(leveling.maximumLevels[name.lowercase()]) { 50 }
+
+ fun getLadder(leveling: Leveling): List<Int> {
+ if (this == SOCIAL) return leveling.socialExperienceRequiredPerLevel
+ if (this == RUNECRAFTING) return leveling.runecraftingExperienceRequiredPerLevel
+ return leveling.skillExperienceRequiredPerLevel
+ }
+}
+
+enum class CollectionCategory(val skill: Skill?, val color: DyeColor, val icon: SkyblockId) {
+ FARMING(Skill.FARMING, DyeColor.YELLOW, SkyblockId("ROOKIE_HOE")),
+ FORAGING(Skill.FORAGING, DyeColor.BROWN, SkyblockId("TREECAPITATOR_AXE")),
+ MINING(Skill.MINING, DyeColor.LIGHT_GRAY, SkyblockId("DIAMOND_PICKAXE")),
+ FISHING(Skill.FISHING, DyeColor.BLUE, SkyblockId("FARMER_ROD")),
+ COMBAT(Skill.COMBAT, DyeColor.RED, SkyblockId("UNDEAD_SWORD")),
+ RIFT(null, DyeColor.PURPLE, SkyblockId("SKYBLOCK_MOTE")),
+}
+
+@Serializable
+@JvmInline
+value class CollectionType(val string: String) {
+ val skyblockId get() = SkyblockId(string.replace(":", "-").replace("MUSHROOM_COLLECTION", "HUGE_MUSHROOM_2"))
+}
+
+@Serializable
+data class Member(
+ val pets: List<Pet> = listOf(),
+ @SerialName("coop_invitation")
+ val coopInvitation: CoopInvitation? = null,
+ @SerialName("experience_skill_farming")
+ val experienceSkillFarming: Double = 0.0,
+ @SerialName("experience_skill_alchemy")
+ val experienceSkillAlchemy: Double = 0.0,
+ @SerialName("experience_skill_combat")
+ val experienceSkillCombat: Double = 0.0,
+ @SerialName("experience_skill_taming")
+ val experienceSkillTaming: Double = 0.0,
+ @SerialName("experience_skill_social2")
+ val experienceSkillSocial: Double = 0.0,
+ @SerialName("experience_skill_enchanting")
+ val experienceSkillEnchanting: Double = 0.0,
+ @SerialName("experience_skill_fishing")
+ val experienceSkillFishing: Double = 0.0,
+ @SerialName("experience_skill_foraging")
+ val experienceSkillForaging: Double = 0.0,
+ @SerialName("experience_skill_mining")
+ val experienceSkillMining: Double = 0.0,
+ @SerialName("experience_skill_runecrafting")
+ val experienceSkillRunecrafting: Double = 0.0,
+ @SerialName("experience_skill_carpentry")
+ val experienceSkillCarpentry: Double = 0.0,
+ val collection: Map<CollectionType, Long> = mapOf()
+)
+
+@Serializable
+data class CoopInvitation(
+ val timestamp: Instant,
+ @SerialName("invited_by")
+ val invitedBy: UUID? = null,
+ val confirmed: Boolean,
+)
+
+@JvmInline
+@Serializable
+value class PetType(val name: String)
+
+@Serializable
+data class Pet(
+ val uuid: UUID? = null,
+ val type: PetType,
+ val exp: Double = 0.0,
+ val active: Boolean = false,
+ val tier: Rarity,
+ val candyUsed: Int = 0,
+ val heldItem: String? = null,
+ val skin: String? = null,
+) {
+ val itemId get() = SkyblockId("${type.name};${tier.ordinal}")
+}
+
+@Serializable
+data class PlayerResponse(
+ val success: Boolean,
+ val player: PlayerData,
+)
+
+@Serializable
+data class PlayerData(
+ val uuid: UUID,
+ val firstLogin: Instant,
+ val lastLogin: Instant? = null,
+ @SerialName("playername")
+ val playerName: String,
+ val achievementsOneTime: List<String> = listOf(),
+ @SerialName("newPackageRank")
+ val packageRank: String? = null,
+ val monthlyPackageRank: String? = null,
+ val rankPlusColor: String = "GOLD"
+) {
+ val rankPlusDyeColor = LegacyFormattingCode.values().find { it.name == rankPlusColor } ?: LegacyFormattingCode.GOLD
+ val rankData get() = RepoManager.neuRepo.constants.misc.ranks[if (monthlyPackageRank == "NONE" || monthlyPackageRank == null) packageRank else monthlyPackageRank]
+ fun getDisplayName(name: String = playerName) = rankData?.let {
+ ("§${it.color}[${it.tag}${rankPlusDyeColor.modern}" +
+ "${it.plus ?: ""}§${it.color}] $name")
+ } ?: "${Formatting.GRAY}$name"
+
+
+}
+
+@Serializable
+data class AshconNameLookup(
+ val username: String,
+ val uuid: UUID,
+)
diff --git a/src/main/kotlin/apis/Routes.kt b/src/main/kotlin/apis/Routes.kt
new file mode 100644
index 0000000..bf55a2d
--- /dev/null
+++ b/src/main/kotlin/apis/Routes.kt
@@ -0,0 +1,95 @@
+
+
+package moe.nea.firmament.apis
+
+import io.ktor.client.call.*
+import io.ktor.client.request.*
+import io.ktor.http.*
+import io.ktor.util.*
+import java.util.*
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.async
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import kotlin.collections.MutableMap
+import kotlin.collections.listOf
+import kotlin.collections.mutableMapOf
+import kotlin.collections.set
+import moe.nea.firmament.Firmament
+import moe.nea.firmament.util.MinecraftDispatcher
+
+object Routes {
+ private val nameToUUID: MutableMap<String, Deferred<UUID?>> = CaseInsensitiveMap()
+ private val profiles: MutableMap<UUID, Deferred<Profiles?>> = mutableMapOf()
+ private val accounts: MutableMap<UUID, Deferred<PlayerData?>> = mutableMapOf()
+ private val UUIDToName: MutableMap<UUID, Deferred<String?>> = mutableMapOf()
+
+ suspend fun getPlayerNameForUUID(uuid: UUID): String? {
+ return withContext(MinecraftDispatcher) {
+ UUIDToName.computeIfAbsent(uuid) {
+ async(Firmament.coroutineScope.coroutineContext) {
+ val response = Firmament.httpClient.get("https://api.ashcon.app/mojang/v2/user/$uuid")
+ if (!response.status.isSuccess()) return@async null
+ val data = response.body<AshconNameLookup>()
+ launch(MinecraftDispatcher) {
+ nameToUUID[data.username] = async { data.uuid }
+ }
+ data.username
+ }
+ }
+ }.await()
+ }
+
+ suspend fun getUUIDForPlayerName(name: String): UUID? {
+ return withContext(MinecraftDispatcher) {
+ nameToUUID.computeIfAbsent(name) {
+ async(Firmament.coroutineScope.coroutineContext) {
+ val response = Firmament.httpClient.get("https://api.ashcon.app/mojang/v2/user/$name")
+ if (!response.status.isSuccess()) return@async null
+ val data = response.body<AshconNameLookup>()
+ launch(MinecraftDispatcher) {
+ UUIDToName[data.uuid] = async { data.username }
+ }
+ data.uuid
+ }
+ }
+ }.await()
+ }
+
+ suspend fun getAccountData(uuid: UUID): PlayerData? {
+ return withContext(MinecraftDispatcher) {
+ accounts.computeIfAbsent(uuid) {
+ async(Firmament.coroutineScope.coroutineContext) {
+ val response = UrsaManager.request(listOf("v1", "hypixel","player", uuid.toString()))
+ if (!response.status.isSuccess()) {
+ launch(MinecraftDispatcher) {
+ @Suppress("DeferredResultUnused")
+ accounts.remove(uuid)
+ }
+ return@async null
+ }
+ response.body<PlayerResponse>().player
+ }
+ }
+ }.await()
+ }
+
+ suspend fun getProfiles(uuid: UUID): Profiles? {
+ return withContext(MinecraftDispatcher) {
+ profiles.computeIfAbsent(uuid) {
+ async(Firmament.coroutineScope.coroutineContext) {
+ val response = UrsaManager.request(listOf("v1", "hypixel","profiles", uuid.toString()))
+ if (!response.status.isSuccess()) {
+ launch(MinecraftDispatcher) {
+ @Suppress("DeferredResultUnused")
+ profiles.remove(uuid)
+ }
+ return@async null
+ }
+ response.body<Profiles>()
+ }
+ }
+ }.await()
+ }
+
+}
diff --git a/src/main/kotlin/apis/UrsaManager.kt b/src/main/kotlin/apis/UrsaManager.kt
new file mode 100644
index 0000000..13f7aef
--- /dev/null
+++ b/src/main/kotlin/apis/UrsaManager.kt
@@ -0,0 +1,72 @@
+
+
+package moe.nea.firmament.apis
+
+import io.ktor.client.request.*
+import io.ktor.client.statement.*
+import io.ktor.http.*
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.withContext
+import moe.nea.firmament.Firmament
+import net.minecraft.client.MinecraftClient
+import java.time.Duration
+import java.time.Instant
+import java.util.*
+
+object UrsaManager {
+ private data class Token(
+ val validUntil: Instant,
+ val token: String,
+ val obtainedFrom: String,
+ ) {
+ fun isValid(host: String) = Instant.now().plusSeconds(60) < validUntil && obtainedFrom == host
+ }
+
+ private var currentToken: Token? = null
+ private val lock = Mutex()
+ private fun getToken(host: String) = currentToken?.takeIf { it.isValid(host) }
+
+ suspend fun request(path: List<String>): HttpResponse {
+ var didLock = false
+ try {
+ val host = "ursa.notenoughupdates.org"
+ var token = getToken(host)
+ if (token == null) {
+ lock.lock()
+ didLock = true
+ token = getToken(host)
+ }
+ val response = Firmament.httpClient.get {
+ url {
+ this.host = host
+ appendPathSegments(path, encodeSlash = true)
+ }
+ if (token == null) {
+ withContext(Dispatchers.IO) {
+ val mc = MinecraftClient.getInstance()
+ val serverId = UUID.randomUUID().toString()
+ mc.sessionService.joinServer(mc.session.uuidOrNull, mc.session.accessToken, serverId)
+ header("x-ursa-username", mc.session.username)
+ header("x-ursa-serverid", serverId)
+ }
+ } else {
+ header("x-ursa-token", token.token)
+ }
+ }
+ val savedToken = response.headers["x-ursa-token"]
+ if (savedToken != null) {
+ val validUntil = response.headers["x-ursa-expires"]?.toLongOrNull()?.let { Instant.ofEpochMilli(it) }
+ ?: (Instant.now() + Duration.ofMinutes(55))
+ currentToken = Token(validUntil, savedToken, host)
+ }
+ if (response.status.value != 200) {
+ Firmament.logger.error("Failed to contact ursa minor: ${response.bodyAsText()}")
+ }
+ return response
+ } finally {
+ if (didLock)
+ lock.unlock()
+ }
+ }
+}