diff options
author | Linnea Gräf <nea@nea.moe> | 2024-08-28 19:04:24 +0200 |
---|---|---|
committer | Linnea Gräf <nea@nea.moe> | 2024-08-28 19:04:24 +0200 |
commit | d2f240ff0ca0d27f417f837e706c781a98c31311 (patch) | |
tree | 0db7aff6cc14deaf36eed83889d59fd6b3a6f599 /src/main/kotlin/apis | |
parent | a6906308163aa3b2d18fa1dc1aa71ac9bbcc83ab (diff) | |
download | firmament-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.kt | 194 | ||||
-rw-r--r-- | src/main/kotlin/apis/Routes.kt | 95 | ||||
-rw-r--r-- | src/main/kotlin/apis/UrsaManager.kt | 72 |
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() + } + } +} |